tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: https://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5<a href="https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"><img src="https://github.com/Tim55667757/TKSBrokerAPI/blob/develop/docs/media/TKSBrokerAPI-Logo.png?raw=true" alt="TKSBrokerAPI-Logo" width="780" target="_blank" /></a> 6 7**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 8as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 9from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 10 11TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 12the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 13 14- **Open account for trading:** https://tinkoff.ru/sl/AaX1Et1omnH 15- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 16- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 17- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 18- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 19- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 20""" 21 22# Copyright (c) 2022 Gilmillin Timur Mansurovich 23# 24# Licensed under the Apache License, Version 2.0 (the "License"); 25# you may not use this file except in compliance with the License. 26# You may obtain a copy of the License at 27# 28# http://www.apache.org/licenses/LICENSE-2.0 29# 30# Unless required by applicable law or agreed to in writing, software 31# distributed under the License is distributed on an "AS IS" BASIS, 32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33# See the License for the specific language governing permissions and 34# limitations under the License. 35 36 37import sys 38import os 39from argparse import ArgumentParser 40from importlib.metadata import version 41 42from dateutil.tz import tzlocal 43from time import sleep 44 45import re 46import json 47import requests 48import traceback as tb 49 50from multiprocessing import cpu_count, Lock 51from multiprocessing.pool import ThreadPool 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self._tag = "" 129 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 130 131 self.__lock = Lock() # initialize multiprocessing mutex lock 132 133 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 134 135 self.aliases = TKS_TICKER_ALIASES 136 """Some aliases instead official tickers. 137 138 See also: `TKSEnums.TKS_TICKER_ALIASES` 139 """ 140 141 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 142 143 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 144 145 self._ticker = "" 146 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 147 148 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 149 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 150 151 See also: `SearchByTicker()`, `SearchInstruments()`. 152 """ 153 154 self._figi = "" 155 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 156 157 See also: `SearchByFIGI()`, `SearchInstruments()`. 158 """ 159 160 self.depth = 1 161 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 162 163 See also: `GetCurrentPrices()`. 164 """ 165 166 self.server = r"https://invest-public-api.tinkoff.ru/rest" 167 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 168 169 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 170 """ 171 172 uLogger.debug("Broker API server: {}".format(self.server)) 173 174 self.timeout = 15 175 """Server operations timeout in seconds. Default: `15`. 176 177 See also: `SendAPIRequest()`. 178 """ 179 180 self.headers = { 181 "Content-Type": "application/json", 182 "accept": "application/json", 183 "Authorization": "Bearer {}".format(self.token), 184 "x-app-name": "Tim55667757.TKSBrokerAPI", 185 } 186 """ 187 Headers which send in every request to broker server. Please, do not change it! 188 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 189 190 See also: `SendAPIRequest()`. 191 """ 192 193 self.body = None 194 """Request body which send to broker server. Default: `None`. 195 196 See also: `SendAPIRequest()`. 197 """ 198 199 self.moreDebug = False 200 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 201 202 self.useHTMLReports = False 203 """ 204 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 205 206 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 207 """ 208 209 self.historyFile = None 210 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 211 212 See also: `History()`. 213 """ 214 215 self.htmlHistoryFile = "index.html" 216 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 217 218 See also: `ShowHistoryChart()`. 219 """ 220 221 self.instrumentsFile = "instruments.md" 222 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 223 224 See also: `ShowInstrumentsInfo()`. 225 """ 226 227 self.searchResultsFile = "search-results.md" 228 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 229 230 See also: `SearchInstruments()`. 231 """ 232 233 self.pricesFile = "prices.md" 234 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 235 236 See also: `GetListOfPrices()`. 237 """ 238 239 self.infoFile = "info.md" 240 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 241 242 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 243 """ 244 245 self.bondsXLSXFile = "ext-bonds.xlsx" 246 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 247 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 248 249 See also: `ExtendBondsData()`. 250 """ 251 252 self.calendarFile = "calendar.md" 253 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 254 255 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 256 257 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 258 """ 259 260 self.overviewFile = "overview.md" 261 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 262 263 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 264 """ 265 266 self.overviewDigestFile = "overview-digest.md" 267 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 268 269 See also: `Overview()` with parameter `details="digest"`. 270 """ 271 272 self.overviewPositionsFile = "overview-positions.md" 273 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 274 275 See also: `Overview()` with parameter `details="positions"`. 276 """ 277 278 self.overviewOrdersFile = "overview-orders.md" 279 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 280 281 See also: `Overview()` with parameter `details="orders"`. 282 """ 283 284 self.overviewAnalyticsFile = "overview-analytics.md" 285 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 286 287 See also: `Overview()` with parameter `details="analytics"`. 288 """ 289 290 self.overviewBondsCalendarFile = "overview-calendar.md" 291 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 292 293 See also: `Overview()` with parameter `details="calendar"`. 294 """ 295 296 self.reportFile = "deals.md" 297 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 298 299 See also: `Deals()`. 300 """ 301 302 self.withdrawalLimitsFile = "limits.md" 303 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 304 305 See also: `OverviewLimits()` and `RequestLimits()`. 306 """ 307 308 self.userInfoFile = "user-info.md" 309 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 310 311 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 312 """ 313 314 self.userAccountsFile = "accounts.md" 315 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 316 317 See also: `OverviewAccounts()`, `RequestAccounts()`. 318 """ 319 320 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 321 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 322 323 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 324 325 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 326 """ 327 328 self.iList = None # init iList for raw instruments data 329 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 330 331 See also: `Listing()`, `DumpInstruments()`. 332 """ 333 334 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 335 if useCache: 336 if os.path.exists(self.iListDumpFile): 337 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 338 curTime = datetime.now(tzutc()) 339 340 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 341 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 342 343 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 344 345 else: 346 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 347 348 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 349 os.path.abspath(self.iListDumpFile), 350 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 351 )) 352 353 else: 354 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 355 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 356 357 else: 358 self.iList = self.Listing() # request new raw instruments data from broker server 359 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 360 361 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 362 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 363 364 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 365 """ 366 367 @property 368 def tag(self) -> str: 369 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 370 return self._tag 371 372 @tag.setter 373 def tag(self, value): 374 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 375 self._tag = str(value) 376 377 if self._tag: 378 for handler in uLogger.handlers: 379 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 380 381 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 382 383 else: 384 for handler in uLogger.handlers: 385 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 386 387 uLogger.debug("Default logger format is used") 388 389 @property 390 def ticker(self) -> str: 391 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 392 393 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 394 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 395 396 See also: `SearchByTicker()`, `SearchInstruments()`. 397 """ 398 return self._ticker 399 400 @ticker.setter 401 def ticker(self, value): 402 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 403 404 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 405 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 406 407 See also: `SearchByTicker()`, `SearchInstruments()`. 408 """ 409 self._ticker = str(value).upper() # Tickers may be upper case only 410 411 @property 412 def figi(self) -> str: 413 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 414 415 See also: `SearchByFIGI()`, `SearchInstruments()`. 416 """ 417 return self._figi 418 419 @figi.setter 420 def figi(self, value): 421 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 422 423 See also: `SearchByFIGI()`, `SearchInstruments()`. 424 """ 425 self._figi = str(value).upper() # FIGI may be upper case only 426 427 def _ParseJSON(self, rawData="{}") -> dict: 428 """ 429 Parse JSON from response string. 430 431 :param rawData: this is a string with JSON-formatted text. 432 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 433 """ 434 try: 435 responseJSON = json.loads(rawData) if rawData else {} 436 437 if self.moreDebug: 438 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 439 440 return responseJSON 441 442 except Exception as e: 443 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 444 445 return {} 446 447 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 448 """ 449 Send GET or POST request to broker server and receive JSON object. 450 451 self.header: must be defining with dictionary of headers. 452 self.body: if define then used as request body. None by default. 453 self.timeout: global request timeout, 15 seconds by default. 454 :param url: url with REST request. 455 :param reqType: send "GET" or "POST" request. "GET" by default. 456 :param retry: how many times retry after first request if an 5xx server errors occurred. 457 :param pause: sleep time in seconds between retries. 458 :return: response JSON (dictionary) from broker. 459 """ 460 if reqType.upper() not in ("GET", "POST"): 461 uLogger.error("You can define request type: `GET` or `POST`!") 462 raise Exception("Incorrect value") 463 464 if self.moreDebug: 465 uLogger.debug("Request parameters:") 466 uLogger.debug(" - REST API URL: {}".format(url)) 467 uLogger.debug(" - request type: {}".format(reqType)) 468 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 469 uLogger.debug(" - body:\n{}".format(self.body)) 470 471 # fast hack to avoid all operations with some tickers/FIGI 472 responseJSON = {} 473 oK = True 474 for item in self.exclude: 475 if item in url: 476 if self.moreDebug: 477 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 478 479 oK = False 480 break 481 482 if oK: 483 with self.__lock: # acquire the mutex lock 484 counter = 0 485 response = None 486 errMsg = "" 487 488 while not response and counter <= retry: 489 if reqType == "GET": 490 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 491 492 if reqType == "POST": 493 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 494 495 if self.moreDebug: 496 uLogger.debug("Response:") 497 uLogger.debug(" - status code: {}".format(response.status_code)) 498 uLogger.debug(" - reason: {}".format(response.reason)) 499 uLogger.debug(" - body length: {}".format(len(response.text))) 500 uLogger.debug(" - headers:\n{}".format(response.headers)) 501 502 # Server returns some headers: 503 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 504 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 505 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 506 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 507 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 508 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 509 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 510 sleep(rateLimitWait) 511 512 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 513 if 400 <= response.status_code < 500: 514 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 515 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 516 517 if "code" in response.text and "message" in response.text: 518 msgDict = self._ParseJSON(rawData=response.text) 519 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 520 521 counter = retry + 1 # do not retry for 4xx errors 522 523 if 500 <= response.status_code < 600: 524 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 525 uLogger.debug(" - not oK, {}".format(errMsg)) 526 527 if "code" in response.text and "message" in response.text: 528 errMsgDict = self._ParseJSON(rawData=response.text) 529 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 530 531 counter += 1 532 533 if counter <= retry: 534 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 535 sleep(pause) 536 537 responseJSON = self._ParseJSON(rawData=response.text) 538 539 if errMsg: 540 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 541 uLogger.error(" - not oK, {}".format(errMsg)) 542 543 return responseJSON 544 545 def _IUpdater(self, iType: str) -> tuple: 546 """ 547 Request instrument by type from server. See available API methods for instruments: 548 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 549 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 550 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 551 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 552 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 553 554 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 555 :return: tuple with iType name and list of available instruments of current type for defined user token. 556 """ 557 result = [] 558 559 if iType in TKS_INSTRUMENTS: 560 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 561 562 # all instruments have the same body in API v2 requests: 563 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 564 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 565 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 566 567 return iType, result 568 569 def _IWrapper(self, kwargs): 570 """ 571 Wrapper runs instrument's update method `_IUpdater()`. 572 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 573 """ 574 return self._IUpdater(**kwargs) 575 576 def Listing(self) -> dict: 577 """ 578 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 579 580 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 581 """ 582 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 583 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 584 585 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 586 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 587 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 588 589 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 590 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 591 poolUpdater.close() # close the thread pool 592 poolUpdater.join() # wait a moment until all data returns from threads 593 594 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 595 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 596 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 597 598 # calculate minimum price increment (step) for all instruments and set up instrument's type: 599 for iType in iList.keys(): 600 for ticker in iList[iType]: 601 iList[iType][ticker]["type"] = iType 602 603 if "minPriceIncrement" in iList[iType][ticker].keys(): 604 iList[iType][ticker]["step"] = NanoToFloat( 605 iList[iType][ticker]["minPriceIncrement"]["units"], 606 iList[iType][ticker]["minPriceIncrement"]["nano"], 607 ) 608 609 else: 610 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 611 612 return iList 613 614 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 615 """ 616 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 617 618 See also: `DumpInstruments()`, `Listing()`. 619 620 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 621 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 622 """ 623 if self.iListDumpFile is None or not self.iListDumpFile: 624 uLogger.error("Output name of dump file must be defined!") 625 raise Exception("Filename required") 626 627 if not self.iList or forceUpdate: 628 self.iList = self.Listing() 629 630 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 631 632 # Save as XLSX with separated sheets for every type of instruments: 633 with pd.ExcelWriter( 634 path=xlsxDumpFile, 635 date_format=TKS_DATE_FORMAT, 636 datetime_format=TKS_DATE_TIME_FORMAT, 637 mode="w", 638 ) as writer: 639 for iType in TKS_INSTRUMENTS: 640 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 641 df = df[sorted(df)] # sorted by column names 642 df = df.applymap( 643 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 644 na_action="ignore", 645 ) # converting numbers from nano-type to float in every cell 646 df.to_excel( 647 writer, 648 sheet_name=iType, 649 encoding="UTF-8", 650 freeze_panes=(1, 1), 651 ) # saving as XLSX-file with freeze first row and column as headers 652 653 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 654 655 def DumpInstruments(self, forceUpdate: bool = True) -> str: 656 """ 657 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 658 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 659 660 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 661 662 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 663 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 664 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 665 """ 666 if self.iListDumpFile is None or not self.iListDumpFile: 667 uLogger.error("Output name of dump file must be defined!") 668 raise Exception("Filename required") 669 670 if not self.iList or forceUpdate: 671 self.iList = self.Listing() 672 673 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 674 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 675 fH.write(jsonDump) 676 677 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 678 679 return jsonDump 680 681 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 682 """ 683 Show information about one instrument defined by json data and prints it in Markdown format. 684 685 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 686 687 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 688 :param show: if `True` then also printing information about instrument and its current price. 689 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 690 :return: multilines text in Markdown format with information about one instrument. 691 """ 692 splitLine = "| | |\n" 693 infoText = "" 694 695 if iJSON is not None and iJSON and isinstance(iJSON, dict): 696 info = [ 697 "# Main information\n\n", 698 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 699 "| Parameters | Values |\n", 700 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 701 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 702 "| Full name: | {:<54} |\n".format(iJSON["name"]), 703 ] 704 705 if "sector" in iJSON.keys() and iJSON["sector"]: 706 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 707 708 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 709 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 710 711 info.extend([ 712 splitLine, 713 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 714 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 715 ]) 716 717 if "isin" in iJSON.keys() and iJSON["isin"]: 718 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 719 720 if "classCode" in iJSON.keys(): 721 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 722 723 info.extend([ 724 splitLine, 725 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 726 splitLine, 727 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 728 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 729 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 730 ]) 731 732 if iJSON["figi"]: 733 self._figi = iJSON["figi"] 734 iJSON = iJSON | self.RequestTradingStatus() 735 736 info.extend([ 737 splitLine, 738 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 739 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 740 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 741 ]) 742 743 info.append(splitLine) 744 745 if "type" in iJSON.keys() and iJSON["type"]: 746 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 747 748 if "shareType" in iJSON.keys() and iJSON["shareType"]: 749 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 750 751 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 752 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 753 754 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 755 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 756 757 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 758 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 759 760 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 761 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 762 763 if "focusType" in iJSON.keys() and iJSON["focusType"]: 764 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 765 766 if "assetType" in iJSON.keys() and iJSON["assetType"]: 767 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 768 769 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 770 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 771 772 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 773 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 774 775 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 776 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 777 778 if "currency" in iJSON.keys(): 779 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 780 781 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 782 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 783 784 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 785 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 786 787 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 788 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 789 790 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 791 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 792 793 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 794 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 795 796 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 797 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 798 799 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 800 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 801 802 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 803 info.append("| Perpetual bond: | Yes |\n") 804 805 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 806 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 807 808 iExt = None 809 if iJSON["type"] == "Bonds": 810 info.extend([ 811 splitLine, 812 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 813 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 814 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 815 iJSON["nominal"]["currency"], 816 )), 817 ]) 818 819 if "floatingCouponFlag" in iJSON.keys(): 820 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 821 822 if "amortizationFlag" in iJSON.keys(): 823 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 824 825 info.append(splitLine) 826 827 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 828 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 829 830 if iJSON["figi"]: 831 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 832 833 info.extend([ 834 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 835 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 836 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 837 ]) 838 839 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 840 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 841 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 842 iJSON["aciValue"]["currency"] 843 ))) 844 845 if "currentPrice" in iJSON.keys(): 846 info.append(splitLine) 847 848 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 849 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 850 851 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 852 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 853 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 854 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 855 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 856 857 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 858 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 859 860 info.extend([ 861 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 862 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 863 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 864 )), 865 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 866 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 867 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 868 )), 869 "| Changes between last deal price and last close | {:<54} |\n".format( 870 "{:.2f}%{}".format( 871 iJSON["currentPrice"]["changes"], 872 " ({}{:.2f} {})".format( 873 "+" if bondChangesDelta > 0 else "", 874 bondChangesDelta, 875 aciCurrency 876 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 877 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 878 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 879 currency 880 ), 881 ) 882 ), 883 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 884 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 885 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 886 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 887 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 888 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 889 )), 890 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 891 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 892 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 893 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 894 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 895 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 896 )), 897 ]) 898 899 if "lot" in iJSON.keys(): 900 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 901 902 if "step" in iJSON.keys() and iJSON["step"] != 0: 903 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 904 905 # Add bond payment calendar: 906 if iJSON["type"] == "Bonds": 907 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 908 info.extend(["\n#", strCalendar]) 909 910 infoText += "".join(info) 911 912 if show and not onlyFiles: 913 uLogger.info("{}".format(infoText)) 914 915 if self.infoFile is not None and (show or onlyFiles): 916 with open(self.infoFile, "w", encoding="UTF-8") as fH: 917 fH.write(infoText) 918 919 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 920 921 if self.useHTMLReports: 922 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 923 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 924 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 925 926 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 927 928 return infoText 929 930 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 931 """ 932 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 933 934 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 935 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 936 :return: JSON formatted data with information about instrument. 937 """ 938 tickerJSON = {} 939 if self.moreDebug: 940 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 941 942 if not self._ticker: 943 uLogger.warning("self._ticker variable is not be empty!") 944 945 else: 946 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 947 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 948 raise Exception("Instrument not allowed") 949 950 if not self.iList: 951 self.iList = self.Listing() 952 953 if self._ticker in self.iList["Shares"].keys(): 954 tickerJSON = self.iList["Shares"][self._ticker] 955 if self.moreDebug: 956 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 957 958 elif self._ticker in self.iList["Currencies"].keys(): 959 tickerJSON = self.iList["Currencies"][self._ticker] 960 if self.moreDebug: 961 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 962 963 elif self._ticker in self.iList["Bonds"].keys(): 964 tickerJSON = self.iList["Bonds"][self._ticker] 965 if self.moreDebug: 966 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 967 968 elif self._ticker in self.iList["Etfs"].keys(): 969 tickerJSON = self.iList["Etfs"][self._ticker] 970 if self.moreDebug: 971 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 972 973 elif self._ticker in self.iList["Futures"].keys(): 974 tickerJSON = self.iList["Futures"][self._ticker] 975 if self.moreDebug: 976 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 977 978 if tickerJSON: 979 self._figi = tickerJSON["figi"] 980 981 if requestPrice: 982 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 983 984 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 985 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 986 987 else: 988 tickerJSON["currentPrice"]["changes"] = 0 989 990 if show: 991 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 992 993 else: 994 if show: 995 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 996 997 return tickerJSON 998 999 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1000 """ 1001 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1002 1003 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1004 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1005 :return: JSON formatted data with information about instrument. 1006 """ 1007 figiJSON = {} 1008 if self.moreDebug: 1009 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1010 1011 if not self._figi: 1012 uLogger.warning("self._figi variable is not be empty!") 1013 1014 else: 1015 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1016 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1017 raise Exception("Instrument not allowed") 1018 1019 if not self.iList: 1020 self.iList = self.Listing() 1021 1022 for item in self.iList["Shares"].keys(): 1023 if self._figi == self.iList["Shares"][item]["figi"]: 1024 figiJSON = self.iList["Shares"][item] 1025 1026 if self.moreDebug: 1027 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1028 1029 break 1030 1031 if not figiJSON: 1032 for item in self.iList["Currencies"].keys(): 1033 if self._figi == self.iList["Currencies"][item]["figi"]: 1034 figiJSON = self.iList["Currencies"][item] 1035 1036 if self.moreDebug: 1037 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Bonds"].keys(): 1043 if self._figi == self.iList["Bonds"][item]["figi"]: 1044 figiJSON = self.iList["Bonds"][item] 1045 1046 if self.moreDebug: 1047 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Etfs"].keys(): 1053 if self._figi == self.iList["Etfs"][item]["figi"]: 1054 figiJSON = self.iList["Etfs"][item] 1055 1056 if self.moreDebug: 1057 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Futures"].keys(): 1063 if self._figi == self.iList["Futures"][item]["figi"]: 1064 figiJSON = self.iList["Futures"][item] 1065 1066 if self.moreDebug: 1067 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1068 1069 break 1070 1071 if figiJSON: 1072 self._figi = figiJSON["figi"] 1073 self._ticker = figiJSON["ticker"] 1074 1075 if requestPrice: 1076 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1077 1078 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1079 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1080 1081 else: 1082 figiJSON["currentPrice"]["changes"] = 0 1083 1084 if show: 1085 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1086 1087 else: 1088 if show: 1089 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1090 1091 return figiJSON 1092 1093 def GetCurrentPrices(self, show: bool = True) -> dict: 1094 """ 1095 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1096 `{"buy": [{"price": 1243.8, "quantity": 193}, 1097 {"price": 1244.0, "quantity": 168}, 1098 {"price": 1244.8, "quantity": 5}, 1099 {"price": 1245.0, "quantity": 61}, 1100 {"price": 1245.4, "quantity": 60}], 1101 "sell": [{"price": 1243.6, "quantity": 8}, 1102 {"price": 1242.6, "quantity": 10}, 1103 {"price": 1242.4, "quantity": 18}, 1104 {"price": 1242.2, "quantity": 50}, 1105 {"price": 1242.0, "quantity": 113}], 1106 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1107 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1108 - sell: list of dicts with Buyers prices, 1109 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1110 - quantity: volume value by current price in lots, 1111 - limitUp: current trade session limit price, maximum, 1112 - limitDown: current trade session limit price, minimum, 1113 - lastPrice: last deal price of the instrument, 1114 - closePrice: previous trade session close price of the instrument. 1115 1116 See also: `SearchByTicker()` and `SearchByFIGI()`. 1117 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1118 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1119 1120 :param show: if `True` then print DOM to log and console. 1121 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1122 If an error occurred then returns an empty record: 1123 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1124 """ 1125 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1126 1127 if self.depth < 1: 1128 uLogger.error("Depth of Market (DOM) must be >=1!") 1129 raise Exception("Incorrect value") 1130 1131 if not (self._ticker or self._figi): 1132 uLogger.error("self._ticker or self._figi variables must be defined!") 1133 raise Exception("Ticker or FIGI required") 1134 1135 if self._ticker and not self._figi: 1136 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1137 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1138 1139 if not self._ticker and self._figi: 1140 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1141 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1142 1143 if not self._figi: 1144 uLogger.error("FIGI is not defined!") 1145 raise Exception("Ticker or FIGI required") 1146 1147 else: 1148 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1149 1150 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1151 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1152 self.body = str({"figi": self._figi, "depth": self.depth}) 1153 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1154 1155 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1156 # list of dicts with sellers orders: 1157 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1158 1159 # list of dicts with buyers orders: 1160 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1161 1162 # max price of instrument at this time: 1163 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1164 1165 # min price of instrument at this time: 1166 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1167 1168 # last price of deal with instrument: 1169 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1170 1171 # last close price of instrument: 1172 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1173 1174 else: 1175 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1176 uLogger.debug("Server response: {}".format(pricesResponse)) 1177 1178 if show: 1179 if prices["buy"] or prices["sell"]: 1180 info = [ 1181 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1182 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1183 self._ticker, 1184 self._figi, 1185 self.depth, 1186 ), 1187 "-" * 60, "\n", 1188 " Orders of Buyers | Orders of Sellers\n", 1189 "-" * 60, "\n", 1190 " Sell prices (volumes) | Buy prices (volumes)\n", 1191 "-" * 60, "\n", 1192 ] 1193 1194 if not prices["buy"]: 1195 info.append(" | No orders!\n") 1196 sumBuy = 0 1197 1198 else: 1199 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1200 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1201 for item in maxMinSorted: 1202 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1203 1204 if not prices["sell"]: 1205 info.append("No orders! |\n") 1206 sumSell = 0 1207 1208 else: 1209 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1210 for item in prices["sell"]: 1211 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1212 1213 info.extend([ 1214 "-" * 60, "\n", 1215 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1216 "-" * 60, "\n", 1217 ]) 1218 1219 infoText = "".join(info) 1220 1221 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1222 1223 else: 1224 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1225 1226 return prices 1227 1228 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1229 """ 1230 This method get and show information about all available broker instruments for current user account. 1231 If `instrumentsFile` string is not empty then also save information to this file. 1232 1233 :param show: if `True` then print results to console, if `False` — print only to file. 1234 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1235 :return: multi-lines string with all available broker instruments. 1236 """ 1237 if not self.iList: 1238 self.iList = self.Listing() 1239 1240 info = [ 1241 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1242 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1243 ] 1244 1245 # add instruments count by type: 1246 for iType in self.iList.keys(): 1247 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1248 1249 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1250 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1251 1252 # generating info tables with all instruments by type: 1253 for iType in self.iList.keys(): 1254 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1255 1256 for instrument in self.iList[iType].keys(): 1257 iName = self.iList[iType][instrument]["name"] # instrument's name 1258 if len(iName) > 57: 1259 iName = "{}...".format(iName[:54]) # right trim for a long string 1260 1261 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1262 self.iList[iType][instrument]["ticker"], 1263 iName, 1264 self.iList[iType][instrument]["figi"], 1265 self.iList[iType][instrument]["currency"], 1266 self.iList[iType][instrument]["lot"], 1267 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1268 )) 1269 1270 infoText = "".join(info) 1271 1272 if show and not onlyFiles: 1273 uLogger.info(infoText) 1274 1275 if self.instrumentsFile and (show or onlyFiles): 1276 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1277 fH.write(infoText) 1278 1279 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1280 1281 if self.useHTMLReports: 1282 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1283 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1284 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1285 1286 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1287 1288 return infoText 1289 1290 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1291 """ 1292 This method search and show information about instruments by part of its ticker, FIGI or name. 1293 If `searchResultsFile` string is not empty then also save information to this file. 1294 1295 :param pattern: string with part of ticker, FIGI or instrument's name. 1296 :param show: if `True` then print results to console, if `False` — return list of result only. 1297 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1298 :return: list of dictionaries with all found instruments. 1299 """ 1300 if not self.iList: 1301 self.iList = self.Listing() 1302 1303 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1304 compiledPattern = re.compile(pattern, re.IGNORECASE) 1305 1306 for iType in self.iList: 1307 for instrument in self.iList[iType].values(): 1308 searchResult = compiledPattern.search(" ".join( 1309 [instrument["ticker"], instrument["figi"], instrument["name"]] 1310 )) 1311 1312 if searchResult: 1313 searchResults[iType][instrument["ticker"]] = instrument 1314 1315 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1316 info = [ 1317 "# Search results\n\n", 1318 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1319 "* **Search pattern:** [{}]\n".format(pattern), 1320 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1321 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1322 ] 1323 infoShort = info[:] 1324 1325 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1326 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1327 skippedLine = "| ... | ... | ... | ... |\n" 1328 1329 if resultsLen == 0: 1330 info.append("\nNo results\n") 1331 infoShort.append("\nNo results\n") 1332 uLogger.warning("No results. Try changing your search pattern.") 1333 1334 else: 1335 for iType in searchResults: 1336 iTypeValuesCount = len(searchResults[iType].values()) 1337 if iTypeValuesCount > 0: 1338 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 1341 for instrument in searchResults[iType].values(): 1342 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1343 instrument["type"], 1344 instrument["ticker"], 1345 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1346 instrument["figi"], 1347 )) 1348 1349 if iTypeValuesCount <= 5: 1350 infoShort.extend(info[-iTypeValuesCount:]) 1351 1352 else: 1353 infoShort.extend(info[-5:]) 1354 infoShort.append(skippedLine) 1355 1356 infoText = "".join(info) 1357 infoTextShort = "".join(infoShort) 1358 1359 if show and not onlyFiles: 1360 uLogger.info(infoTextShort) 1361 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1362 1363 if self.searchResultsFile and (show or onlyFiles): 1364 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1365 fH.write(infoText) 1366 1367 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1368 1369 if self.useHTMLReports: 1370 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1371 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1372 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1373 1374 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1375 1376 return searchResults 1377 1378 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1379 """ 1380 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1381 1382 :param instruments: list of strings with tickers or FIGIs. 1383 :return: list with unique instrument FIGIs only. 1384 """ 1385 requestedInstruments = [] 1386 for iName in instruments: 1387 if iName not in self.aliases.keys(): 1388 if iName not in requestedInstruments: 1389 requestedInstruments.append(iName) 1390 1391 else: 1392 if iName not in requestedInstruments: 1393 if self.aliases[iName] not in requestedInstruments: 1394 requestedInstruments.append(self.aliases[iName]) 1395 1396 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1397 1398 onlyUniqueFIGIs = [] 1399 for iName in requestedInstruments: 1400 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1401 continue 1402 1403 self._ticker = iName 1404 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1405 1406 if not iData: 1407 self._ticker = "" 1408 self._figi = iName 1409 1410 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1411 1412 if not iData: 1413 self._figi = "" 1414 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1415 1416 if iData and iData["figi"] not in onlyUniqueFIGIs: 1417 onlyUniqueFIGIs.append(iData["figi"]) 1418 1419 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1420 1421 return onlyUniqueFIGIs 1422 1423 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1424 """ 1425 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1426 1427 See limits: https://tinkoff.github.io/investAPI/limits/ 1428 1429 If `pricesFile` string is not empty then also save information to this file. 1430 1431 :param instruments: list of strings with tickers or FIGIs. 1432 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1433 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1434 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1435 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1436 """ 1437 if instruments is None or not instruments: 1438 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1439 raise Exception("Ticker or FIGI required") 1440 1441 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1442 1443 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1444 1445 iList = [] # trying to get info and current prices about all unique instruments: 1446 for self._figi in onlyUniqueFIGIs: 1447 iData = self.SearchByFIGI(requestPrice=True, show=False) 1448 iList.append(iData) 1449 1450 self.ShowListOfPrices(iList, show, onlyFiles) 1451 1452 return iList 1453 1454 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1455 """ 1456 Show table contains current prices of given instruments. 1457 1458 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1459 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1460 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1461 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1462 :return: multilines text in Markdown format as a table contains current prices. 1463 """ 1464 infoText = "" 1465 1466 if show or self.pricesFile or onlyFiles: 1467 info = [ 1468 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1469 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1470 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1471 ] 1472 1473 for item in iList: 1474 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1475 item["ticker"], 1476 item["figi"], 1477 item["type"], 1478 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1479 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1480 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1481 "{} / {}".format( 1482 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1483 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1484 ), 1485 "{} / {}".format( 1486 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1487 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1488 ), 1489 item["currency"], 1490 )) 1491 1492 infoText = "".join(info) 1493 1494 if show and not onlyFiles: 1495 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1496 1497 if self.pricesFile and (show or onlyFiles): 1498 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1499 fH.write(infoText) 1500 1501 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1502 1503 if self.useHTMLReports: 1504 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1505 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1506 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1507 1508 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1509 1510 return infoText 1511 1512 def RequestTradingStatus(self) -> dict: 1513 """ 1514 Requesting trading status for the instrument defined by `figi` variable. 1515 1516 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1517 1518 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1519 1520 :return: dictionary with trading status attributes. Response example: 1521 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1522 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1523 """ 1524 if self._figi is None or not self._figi: 1525 uLogger.error("Variable `figi` must be defined for using this method!") 1526 raise Exception("FIGI required") 1527 1528 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1529 1530 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1531 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1532 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1533 1534 if self.moreDebug: 1535 uLogger.debug("Records about current trading status successfully received") 1536 1537 return tradingStatus 1538 1539 def RequestPortfolio(self) -> dict: 1540 """ 1541 Requesting actual user's portfolio for current `accountId`. 1542 1543 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1544 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1546 1547 :return: dictionary with user's portfolio. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1557 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1558 1559 if self.moreDebug: 1560 uLogger.debug("Records about user's portfolio successfully received") 1561 1562 return rawPortfolio 1563 1564 def RequestPositions(self) -> dict: 1565 """ 1566 Requesting open positions by currencies and instruments for current `accountId`. 1567 1568 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1569 1570 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1571 1572 :return: dictionary with open positions by instruments. 1573 """ 1574 if self.accountId is None or not self.accountId: 1575 uLogger.error("Variable `accountId` must be defined for using this method!") 1576 raise Exception("Account ID required") 1577 1578 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1579 1580 self.body = str({"accountId": self.accountId}) 1581 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1582 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1583 1584 if self.moreDebug: 1585 uLogger.debug("Records about current open positions successfully received") 1586 1587 return rawPositions 1588 1589 def RequestPendingOrders(self) -> list: 1590 """ 1591 Requesting current actual pending limit orders for current `accountId`. 1592 1593 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1594 1595 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1596 1597 :return: list of dictionaries with pending limit orders. 1598 """ 1599 if self.accountId is None or not self.accountId: 1600 uLogger.error("Variable `accountId` must be defined for using this method!") 1601 raise Exception("Account ID required") 1602 1603 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1604 1605 self.body = str({"accountId": self.accountId}) 1606 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1607 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1608 1609 if "orders" in rawResponse.keys(): 1610 rawOrders = rawResponse["orders"] 1611 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1612 1613 else: 1614 rawOrders = [] 1615 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1616 1617 return rawOrders 1618 1619 def RequestStopOrders(self) -> list: 1620 """ 1621 Requesting current actual stop orders for current `accountId`. 1622 1623 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1624 1625 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1626 1627 :return: list of dictionaries with stop orders. 1628 """ 1629 if self.accountId is None or not self.accountId: 1630 uLogger.error("Variable `accountId` must be defined for using this method!") 1631 raise Exception("Account ID required") 1632 1633 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1634 1635 self.body = str({"accountId": self.accountId}) 1636 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1637 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1638 1639 if "stopOrders" in rawResponse.keys(): 1640 rawStopOrders = rawResponse["stopOrders"] 1641 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1642 1643 else: 1644 rawStopOrders = [] 1645 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1646 1647 return rawStopOrders 1648 1649 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1650 """ 1651 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1652 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1653 and `overviewBondsCalendarFile` are defined then also save information to file. 1654 1655 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1656 many requests about the state of the portfolio, and then, based on the received data, a large number 1657 of calculation and statistics are collected. 1658 1659 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1660 :param details: how detailed should the information be? 1661 - `full` — shows full available information about portfolio status (by default), 1662 - `positions` — shows only open positions, 1663 - `orders` — shows only sections of open limits and stop orders. 1664 - `digest` — show a short digest of the portfolio status, 1665 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1666 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1667 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1668 :return: dictionary with client's raw portfolio and some statistics. 1669 """ 1670 if self.accountId is None or not self.accountId: 1671 uLogger.error("Variable `accountId` must be defined for using this method!") 1672 raise Exception("Account ID required") 1673 1674 view = { 1675 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1676 "headers": {}, # list of dictionaries, response headers without "positions" section 1677 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1678 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1679 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1680 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1681 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1682 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1683 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1684 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1685 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1686 }, 1687 "stat": { # --- some statistics calculated using "raw" sections: 1688 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1689 "availableRUB": 0., # available rubles (without other currencies) 1690 "blockedRUB": 0., # blocked sum in Russian Rouble 1691 "totalChangesRUB": 0., # changes for all open trades in RUB 1692 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1693 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1694 "sharesCostRUB": 0., # costs of all shares in RUB 1695 "bondsCostRUB": 0., # costs of all bonds in RUB 1696 "etfsCostRUB": 0., # costs of all etfs in RUB 1697 "futuresCostRUB": 0., # costs of all futures in RUB 1698 "Currencies": [], # list of dictionaries of all currencies statistics 1699 "Shares": [], # list of dictionaries of all shares statistics 1700 "Bonds": [], # list of dictionaries of all bonds statistics 1701 "Etfs": [], # list of dictionaries of all etfs statistics 1702 "Futures": [], # list of dictionaries of all futures statistics 1703 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1704 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1705 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1706 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1707 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1708 }, 1709 "analytics": { # --- some analytics of portfolio: 1710 "distrByAssets": {}, # portfolio distribution by assets 1711 "distrByCompanies": {}, # portfolio distribution by companies 1712 "distrBySectors": {}, # portfolio distribution by sectors 1713 "distrByCurrencies": {}, # portfolio distribution by currencies 1714 "distrByCountries": {}, # portfolio distribution by countries 1715 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1716 } 1717 } 1718 1719 details = details.lower() 1720 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1721 if details not in availableDetails: 1722 details = "full" 1723 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1724 1725 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1726 1727 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1728 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1729 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1730 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1731 1732 # save response headers without "positions" section: 1733 for key in portfolioResponse.keys(): 1734 if key != "positions": 1735 view["raw"]["headers"][key] = portfolioResponse[key] 1736 1737 else: 1738 continue 1739 1740 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1741 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1742 for item in portfolioResponse["positions"]: 1743 if item["instrumentType"] == "currency": 1744 self._figi = item["figi"] 1745 if not self._figi and item["ticker"]: 1746 self._ticker = item["ticker"] 1747 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1748 1749 curr = self.SearchByFIGI(requestPrice=False) 1750 1751 # current price of currency in RUB: 1752 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1753 "name": curr["name"], 1754 "currentPrice": NanoToFloat( 1755 item["currentPrice"]["units"], 1756 item["currentPrice"]["nano"] 1757 ), 1758 } 1759 1760 view["raw"]["Currencies"].append(item) 1761 1762 elif item["instrumentType"] == "share": 1763 view["raw"]["Shares"].append(item) 1764 1765 elif item["instrumentType"] == "bond": 1766 view["raw"]["Bonds"].append(item) 1767 1768 elif item["instrumentType"] == "etf": 1769 view["raw"]["Etfs"].append(item) 1770 1771 elif item["instrumentType"] == "futures": 1772 view["raw"]["Futures"].append(item) 1773 1774 else: 1775 continue 1776 1777 # how many volume of currencies (by ISO currency name) are blocked: 1778 for item in view["raw"]["positions"]["blocked"]: 1779 blocked = NanoToFloat(item["units"], item["nano"]) 1780 if blocked > 0: 1781 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1782 1783 # how many volume of instruments (by FIGI) are blocked: 1784 for item in view["raw"]["positions"]["securities"]: 1785 blocked = int(item["blocked"]) 1786 if blocked > 0: 1787 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1788 1789 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1790 1791 if "rub" in allBlocked.keys(): 1792 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1793 1794 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1795 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1796 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1797 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1798 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1799 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1800 view["stat"]["portfolioCostRUB"] = sum([ 1801 view["stat"]["allCurrenciesCostRUB"], 1802 view["stat"]["sharesCostRUB"], 1803 view["stat"]["bondsCostRUB"], 1804 view["stat"]["etfsCostRUB"], 1805 view["stat"]["futuresCostRUB"], 1806 ]) 1807 1808 # --- calculating some portfolio statistics: 1809 byComp = {} # distribution by companies 1810 bySect = {} # distribution by sectors 1811 byCurr = {} # distribution by currencies (include RUB) 1812 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1813 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1814 1815 for item in portfolioResponse["positions"]: 1816 self._figi = item["figi"] 1817 if not self._figi and item["ticker"]: 1818 self._ticker = item["ticker"] 1819 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1820 1821 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1822 1823 if instrument: 1824 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1825 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1826 1827 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1828 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1829 1830 else: 1831 blocked = 0 1832 1833 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1834 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1835 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1836 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1837 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1838 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1839 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1840 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1841 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1842 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1843 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1844 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1845 1846 statData = { 1847 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1848 "ticker": instrument["ticker"], # ticker by FIGI 1849 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1850 "volume": volume, # available volume of instrument 1851 "lots": lots, # volume in lots of instrument 1852 "direction": direction, # direction of an instrument's position: short or long 1853 "blocked": blocked, # blocked volume of currency or instrument 1854 "currentPrice": curPrice, # current instrument's price in basic asset 1855 "average": average, # current average position price 1856 "cost": cost, # current cost of all volume of instrument in basic asset 1857 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1858 "costRUB": costRUB, # cost of instrument in ruble 1859 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1860 "profit": profit, # expected profit at current moment 1861 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1862 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1863 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1864 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1865 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1866 "step": instrument["step"], # minimum price increment 1867 } 1868 1869 # adding distribution by unique countries: 1870 if statData["country"] not in byCountry.keys(): 1871 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1872 1873 else: 1874 byCountry[statData["country"]]["cost"] += costRUB 1875 byCountry[statData["country"]]["percent"] += percentCostRUB 1876 1877 if item["instrumentType"] != "currency": 1878 # adding distribution by unique companies: 1879 if statData["name"]: 1880 if statData["name"] not in byComp.keys(): 1881 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1882 1883 else: 1884 byComp[statData["name"]]["cost"] += costRUB 1885 byComp[statData["name"]]["percent"] += percentCostRUB 1886 1887 # adding distribution by unique sectors: 1888 if statData["sector"] not in bySect.keys(): 1889 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1890 1891 else: 1892 bySect[statData["sector"]]["cost"] += costRUB 1893 bySect[statData["sector"]]["percent"] += percentCostRUB 1894 1895 # adding distribution by unique currencies: 1896 if currency not in byCurr.keys(): 1897 byCurr[currency] = { 1898 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1899 "cost": costRUB, 1900 "percent": percentCostRUB 1901 } 1902 1903 else: 1904 byCurr[currency]["cost"] += costRUB 1905 byCurr[currency]["percent"] += percentCostRUB 1906 1907 # saving statistics for every instrument: 1908 if item["instrumentType"] == "currency": 1909 view["stat"]["Currencies"].append(statData) 1910 1911 # update dict with free funds for trading (total - blocked) by currencies 1912 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1913 view["stat"]["funds"][currency] = { 1914 "total": volume, 1915 "totalCostRUB": costRUB, # total volume cost in rubles 1916 "free": volume - blocked, 1917 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1918 } 1919 1920 elif item["instrumentType"] == "share": 1921 view["stat"]["Shares"].append(statData) 1922 1923 elif item["instrumentType"] == "bond": 1924 view["stat"]["Bonds"].append(statData) 1925 1926 elif item["instrumentType"] == "etf": 1927 view["stat"]["Etfs"].append(statData) 1928 1929 elif item["instrumentType"] == "Futures": 1930 view["stat"]["Futures"].append(statData) 1931 1932 else: 1933 continue 1934 1935 # total changes in Russian Ruble: 1936 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1937 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1938 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1939 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1940 view["stat"]["funds"]["rub"] = { 1941 "total": view["stat"]["availableRUB"], 1942 "totalCostRUB": view["stat"]["availableRUB"], 1943 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1944 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1945 } 1946 1947 # --- pending limit orders sector data: 1948 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1949 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1950 1951 for item in view["raw"]["orders"]: 1952 self._figi = item["figi"] 1953 1954 if item["figi"] not in uniquePendingOrdersFIGIs: 1955 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1956 1957 uniquePendingOrdersFIGIs.append(item["figi"]) 1958 uniquePendingOrders[item["figi"]] = instrument 1959 1960 else: 1961 instrument = uniquePendingOrders[item["figi"]] 1962 1963 if instrument: 1964 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1965 orderType = TKS_ORDER_TYPES[item["orderType"]] 1966 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1967 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1968 1969 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1970 if item["direction"] == "ORDER_DIRECTION_BUY": 1971 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1972 1973 else: 1974 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1975 1976 # requested price for order execution: 1977 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1978 1979 # necessary changes in percent to reach target from current price: 1980 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1981 1982 view["stat"]["orders"].append({ 1983 "orderID": item["orderId"], # orderId number parameter of current order 1984 "figi": item["figi"], # FIGI identification 1985 "ticker": instrument["ticker"], # ticker name by FIGI 1986 "lotsRequested": item["lotsRequested"], # requested lots value 1987 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1988 "currentPrice": lastPrice, # current instrument's price for defined action 1989 "targetPrice": target, # requested price for order execution in base currency 1990 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1991 "percentChanges": changes, # changes in percent to target from current price 1992 "currency": item["currency"], # instrument's currency name 1993 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1994 "type": orderType, # type of order from TKS_ORDER_TYPES 1995 "status": orderState, # order status from TKS_ORDER_STATES 1996 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1997 }) 1998 1999 # --- stop orders sector data: 2000 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2001 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2002 2003 for item in view["raw"]["stopOrders"]: 2004 self._figi = item["figi"] 2005 2006 if item["figi"] not in uniqueStopOrdersFIGIs: 2007 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2008 2009 uniqueStopOrdersFIGIs.append(item["figi"]) 2010 uniqueStopOrders[item["figi"]] = instrument 2011 2012 else: 2013 instrument = uniqueStopOrders[item["figi"]] 2014 2015 if instrument: 2016 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2017 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2018 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2019 2020 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2021 if "expirationTime" in item.keys(): 2022 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2023 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2024 2025 else: 2026 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2027 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2028 2029 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2030 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2031 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2032 2033 else: 2034 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2035 2036 # requested price when stop-order executed: 2037 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2038 2039 # price for limit-order, set up when stop-order executed: 2040 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2041 2042 # necessary changes in percent to reach target from current price: 2043 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2044 2045 view["stat"]["stopOrders"].append({ 2046 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2047 "figi": item["figi"], # FIGI identification 2048 "ticker": instrument["ticker"], # ticker name by FIGI 2049 "lotsRequested": item["lotsRequested"], # requested lots value 2050 "currentPrice": lastPrice, # current instrument's price for defined action 2051 "targetPrice": target, # requested price for stop-order execution in base currency 2052 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2053 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2054 "percentChanges": changes, # changes in percent to target from current price 2055 "currency": item["currency"], # instrument's currency name 2056 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2057 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2058 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2059 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2060 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2061 }) 2062 2063 # --- calculating data for analytics section: 2064 # portfolio distribution by assets: 2065 view["analytics"]["distrByAssets"] = { 2066 "Ruble": { 2067 "uniques": 1, 2068 "cost": view["stat"]["availableRUB"], 2069 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2070 }, 2071 "Currencies": { 2072 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2073 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2074 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2075 }, 2076 "Shares": { 2077 "uniques": len(view["stat"]["Shares"]), 2078 "cost": view["stat"]["sharesCostRUB"], 2079 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2080 }, 2081 "Bonds": { 2082 "uniques": len(view["stat"]["Bonds"]), 2083 "cost": view["stat"]["bondsCostRUB"], 2084 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2085 }, 2086 "Etfs": { 2087 "uniques": len(view["stat"]["Etfs"]), 2088 "cost": view["stat"]["etfsCostRUB"], 2089 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2090 }, 2091 "Futures": { 2092 "uniques": len(view["stat"]["Futures"]), 2093 "cost": view["stat"]["futuresCostRUB"], 2094 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2095 }, 2096 } 2097 2098 # portfolio distribution by companies: 2099 view["analytics"]["distrByCompanies"]["All money cash"] = { 2100 "ticker": "", 2101 "cost": view["stat"]["allCurrenciesCostRUB"], 2102 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2103 } 2104 view["analytics"]["distrByCompanies"].update(byComp) 2105 2106 # portfolio distribution by sectors: 2107 view["analytics"]["distrBySectors"]["All money cash"] = { 2108 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2109 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2110 } 2111 view["analytics"]["distrBySectors"].update(bySect) 2112 2113 # portfolio distribution by currencies: 2114 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2115 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2116 2117 if self.moreDebug: 2118 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2119 2120 view["analytics"]["distrByCurrencies"].update(byCurr) 2121 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2122 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2123 2124 # portfolio distribution by countries: 2125 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2126 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2127 2128 if self.moreDebug: 2129 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2130 2131 view["analytics"]["distrByCountries"].update(byCountry) 2132 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2133 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2134 2135 # --- Prepare text statistics overview in human-readable: 2136 if show or onlyFiles: 2137 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2138 2139 # Whatever the value `details`, header not changes: 2140 info = [ 2141 "# Client's portfolio\n\n", 2142 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2143 "* **Account ID:** [{}]\n".format(self.accountId), 2144 ] 2145 2146 if details in ["full", "positions", "digest"]: 2147 info.extend([ 2148 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2149 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2150 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2151 view["stat"]["totalChangesRUB"], 2152 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2153 view["stat"]["totalChangesPercentRUB"], 2154 ), 2155 ]) 2156 2157 if details in ["full", "positions"]: 2158 info.extend([ 2159 "## Open positions\n\n", 2160 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2161 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2162 "| **Ruble:** | {:>31} | | | | | |\n".format( 2163 "{:.2f} ({:.2f}) rub".format( 2164 view["stat"]["availableRUB"], 2165 view["stat"]["blockedRUB"], 2166 ) 2167 ) 2168 ]) 2169 2170 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2171 return [ 2172 "| | | | | | | |\n", 2173 "| {:<27} | | | | | {:>19} | |\n".format( 2174 noTradeStr if noTradeStr else typeStr, 2175 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2176 ), 2177 ] 2178 2179 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2180 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2181 "{} [{}]".format(data["ticker"], data["figi"]), 2182 "{:.2f} ({:.2f}) {}".format( 2183 data["volume"], 2184 data["blocked"], 2185 data["currency"], 2186 ) if isCurr else "{:.0f} ({:.0f})".format( 2187 data["volume"], 2188 data["blocked"], 2189 ), 2190 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2191 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2192 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2193 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2194 "{}{:.2f} {} ({}{:.2f}%)".format( 2195 "+" if data["profit"] > 0 else "", 2196 data["profit"], data["baseCurrencyName"], 2197 "+" if data["percentProfit"] > 0 else "", 2198 data["percentProfit"], 2199 ), 2200 ) 2201 2202 # --- Show currencies section: 2203 if view["stat"]["Currencies"]: 2204 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2205 for item in view["stat"]["Currencies"]: 2206 info.append(_InfoStr(item, isCurr=True)) 2207 2208 else: 2209 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2210 2211 # --- Show shares section: 2212 if view["stat"]["Shares"]: 2213 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2214 2215 for item in view["stat"]["Shares"]: 2216 info.append(_InfoStr(item)) 2217 2218 else: 2219 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2220 2221 # --- Show bonds section: 2222 if view["stat"]["Bonds"]: 2223 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2224 2225 for item in view["stat"]["Bonds"]: 2226 info.append(_InfoStr(item)) 2227 2228 else: 2229 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2230 2231 # --- Show etfs section: 2232 if view["stat"]["Etfs"]: 2233 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2234 2235 for item in view["stat"]["Etfs"]: 2236 info.append(_InfoStr(item)) 2237 2238 else: 2239 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2240 2241 # --- Show futures section: 2242 if view["stat"]["Futures"]: 2243 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2244 2245 for item in view["stat"]["Futures"]: 2246 info.append(_InfoStr(item)) 2247 2248 else: 2249 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2250 2251 if details in ["full", "orders"]: 2252 # --- Show pending limit orders section: 2253 if view["stat"]["orders"]: 2254 info.extend([ 2255 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2256 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2257 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2258 ]) 2259 2260 for item in view["stat"]["orders"]: 2261 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2262 "{} [{}]".format(item["ticker"], item["figi"]), 2263 item["orderID"], 2264 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2265 "{} {} ({}{:.2f}%)".format( 2266 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2267 item["baseCurrencyName"], 2268 "+" if item["percentChanges"] > 0 else "", 2269 float(item["percentChanges"]), 2270 ), 2271 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2272 item["action"], 2273 item["type"], 2274 item["date"], 2275 )) 2276 2277 else: 2278 info.append("\n## Total pending limit-orders: [0]\n") 2279 2280 # --- Show stop orders section: 2281 if view["stat"]["stopOrders"]: 2282 info.extend([ 2283 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2284 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2285 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2286 ]) 2287 2288 for item in view["stat"]["stopOrders"]: 2289 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2290 "{} [{}]".format(item["ticker"], item["figi"]), 2291 item["orderID"], 2292 item["lotsRequested"], 2293 "{} {} ({}{:.2f}%)".format( 2294 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2295 item["baseCurrencyName"], 2296 "+" if item["percentChanges"] > 0 else "", 2297 float(item["percentChanges"]), 2298 ), 2299 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2300 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2301 item["action"], 2302 item["type"], 2303 item["expType"], 2304 item["createDate"], 2305 item["expDate"], 2306 )) 2307 2308 else: 2309 info.append("\n## Total stop-orders: [0]\n") 2310 2311 if details in ["full", "analytics"]: 2312 # -- Show analytics section: 2313 if view["stat"]["portfolioCostRUB"] > 0: 2314 info.extend([ 2315 "\n# Analytics\n\n" 2316 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2317 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2318 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2319 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2320 view["stat"]["totalChangesRUB"], 2321 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2322 view["stat"]["totalChangesPercentRUB"], 2323 ), 2324 "\n## Portfolio distribution by assets\n" 2325 "\n| Type | Uniques | Percent | Current cost |\n", 2326 "|------------------------------------|---------|---------|--------------------|\n", 2327 ]) 2328 2329 for key in view["analytics"]["distrByAssets"].keys(): 2330 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2331 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2332 key, 2333 view["analytics"]["distrByAssets"][key]["uniques"], 2334 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2335 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2336 )) 2337 2338 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2339 2340 info.extend([ 2341 "\n## Portfolio distribution by companies\n" 2342 "\n| Company | Percent | Current cost |\n", 2343 aSepLine, 2344 ]) 2345 2346 for company in view["analytics"]["distrByCompanies"].keys(): 2347 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2348 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2349 "{}{}".format( 2350 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2351 company, 2352 ), 2353 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2354 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2355 )) 2356 2357 info.extend([ 2358 "\n## Portfolio distribution by sectors\n" 2359 "\n| Sector | Percent | Current cost |\n", 2360 aSepLine, 2361 ]) 2362 2363 for sector in view["analytics"]["distrBySectors"].keys(): 2364 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2365 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2366 sector, 2367 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2368 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2369 )) 2370 2371 info.extend([ 2372 "\n## Portfolio distribution by currencies\n" 2373 "\n| Instruments currencies | Percent | Current cost |\n", 2374 aSepLine, 2375 ]) 2376 2377 for curr in view["analytics"]["distrByCurrencies"].keys(): 2378 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2379 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2380 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2381 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2382 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2383 )) 2384 2385 info.extend([ 2386 "\n## Portfolio distribution by countries\n" 2387 "\n| Assets by country | Percent | Current cost |\n", 2388 aSepLine, 2389 ]) 2390 2391 for country in view["analytics"]["distrByCountries"].keys(): 2392 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2393 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2394 country, 2395 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2396 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2397 )) 2398 2399 if details in ["full", "calendar"]: 2400 # -- Show bonds payment calendar section: 2401 if view["stat"]["Bonds"]: 2402 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2403 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2404 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2405 2406 else: 2407 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2408 2409 infoText = "".join(info) 2410 2411 if show and not onlyFiles: 2412 uLogger.info(infoText) 2413 2414 if details == "full" and self.overviewFile: 2415 filename = self.overviewFile 2416 2417 elif details == "digest" and self.overviewDigestFile: 2418 filename = self.overviewDigestFile 2419 2420 elif details == "positions" and self.overviewPositionsFile: 2421 filename = self.overviewPositionsFile 2422 2423 elif details == "orders" and self.overviewOrdersFile: 2424 filename = self.overviewOrdersFile 2425 2426 elif details == "analytics" and self.overviewAnalyticsFile: 2427 filename = self.overviewAnalyticsFile 2428 2429 elif details == "calendar" and self.overviewBondsCalendarFile: 2430 filename = self.overviewBondsCalendarFile 2431 2432 else: 2433 filename = "" 2434 2435 if filename and (show or onlyFiles): 2436 with open(filename, "w", encoding="UTF-8") as fH: 2437 fH.write(infoText) 2438 2439 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2440 2441 if self.useHTMLReports: 2442 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2443 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2444 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2445 2446 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2447 2448 return view 2449 2450 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2451 """ 2452 Returns history operations between two given dates for current `accountId`. 2453 If `reportFile` string is not empty then also save human-readable report. 2454 Shows some statistical data of closed positions. 2455 2456 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2457 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2458 :param show: if `True` then also prints all records to the console. 2459 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2460 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2461 :return: original list of dictionaries with history of deals records from API ("operations" key): 2462 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2463 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2464 """ 2465 if self.accountId is None or not self.accountId: 2466 uLogger.error("Variable `accountId` must be defined for using this method!") 2467 raise Exception("Account ID required") 2468 2469 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2470 2471 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2472 2473 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2474 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2475 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2476 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2477 customStat = {} # custom statistics in additional to responseJSON 2478 2479 # --- output report in human-readable format: 2480 if self.reportFile and (show or onlyFiles): 2481 splitLine1 = "| | | | | |\n" # Summary section 2482 splitLine2 = "| | | | | | | | |\n" # Operations section 2483 nextDay = "" 2484 2485 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2486 2487 if len(ops) > 0: 2488 customStat = { 2489 "opsCount": 0, # total operations count 2490 "buyCount": 0, # buy operations 2491 "sellCount": 0, # sell operations 2492 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2493 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2494 "payIn": {"rub": 0.}, # Deposit brokerage account 2495 "payOut": {"rub": 0.}, # Withdrawals 2496 "divs": {"rub": 0.}, # Dividends income 2497 "coupons": {"rub": 0.}, # Coupon's income 2498 "brokerCom": {"rub": 0.}, # Service commissions 2499 "serviceCom": {"rub": 0.}, # Service commissions 2500 "marginCom": {"rub": 0.}, # Margin commissions 2501 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2502 } 2503 2504 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2505 for item in ops: 2506 if item["state"] == "OPERATION_STATE_EXECUTED": 2507 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2508 2509 # count buy operations: 2510 if "_BUY" in item["operationType"]: 2511 customStat["buyCount"] += 1 2512 2513 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2514 customStat["buyTotal"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["buyTotal"][item["payment"]["currency"]] = payment 2518 2519 # count sell operations: 2520 elif "_SELL" in item["operationType"]: 2521 customStat["sellCount"] += 1 2522 2523 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2524 customStat["sellTotal"][item["payment"]["currency"]] += payment 2525 2526 else: 2527 customStat["sellTotal"][item["payment"]["currency"]] = payment 2528 2529 # count incoming operations: 2530 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2531 if item["payment"]["currency"] in customStat["payIn"].keys(): 2532 customStat["payIn"][item["payment"]["currency"]] += payment 2533 2534 else: 2535 customStat["payIn"][item["payment"]["currency"]] = payment 2536 2537 # count withdrawals operations: 2538 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2539 if item["payment"]["currency"] in customStat["payOut"].keys(): 2540 customStat["payOut"][item["payment"]["currency"]] += payment 2541 2542 else: 2543 customStat["payOut"][item["payment"]["currency"]] = payment 2544 2545 # count dividends income: 2546 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2547 if item["payment"]["currency"] in customStat["divs"].keys(): 2548 customStat["divs"][item["payment"]["currency"]] += payment 2549 2550 else: 2551 customStat["divs"][item["payment"]["currency"]] = payment 2552 2553 # count coupon's income: 2554 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2555 if item["payment"]["currency"] in customStat["coupons"].keys(): 2556 customStat["coupons"][item["payment"]["currency"]] += payment 2557 2558 else: 2559 customStat["coupons"][item["payment"]["currency"]] = payment 2560 2561 # count broker commissions: 2562 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2563 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2564 customStat["brokerCom"][item["payment"]["currency"]] += payment 2565 2566 else: 2567 customStat["brokerCom"][item["payment"]["currency"]] = payment 2568 2569 # count service commissions: 2570 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2571 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2572 customStat["serviceCom"][item["payment"]["currency"]] += payment 2573 2574 else: 2575 customStat["serviceCom"][item["payment"]["currency"]] = payment 2576 2577 # count margin commissions: 2578 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2579 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2580 customStat["marginCom"][item["payment"]["currency"]] += payment 2581 2582 else: 2583 customStat["marginCom"][item["payment"]["currency"]] = payment 2584 2585 # count withholding taxes: 2586 elif "_TAX" in item["operationType"]: 2587 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2588 customStat["allTaxes"][item["payment"]["currency"]] += payment 2589 2590 else: 2591 customStat["allTaxes"][item["payment"]["currency"]] = payment 2592 2593 else: 2594 continue 2595 2596 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2597 2598 # --- view "Actions" lines: 2599 info.extend([ 2600 "| Report sections | | | | |\n", 2601 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2602 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2603 "| | Buy: {:<22} | {:<28} | | |\n".format( 2604 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2605 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2606 ), 2607 "| | Sell: {:<21} | {:<28} | | |\n".format( 2608 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2609 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2610 ), 2611 ]) 2612 2613 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2614 for key in opsKeys: 2615 if key == "rub": 2616 continue 2617 2618 info.extend([ 2619 "| | | {:<28} | | |\n".format( 2620 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2621 ), 2622 "| | | {:<28} | | |\n".format( 2623 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2624 ), 2625 ]) 2626 2627 info.append(splitLine1) 2628 2629 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2630 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2631 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2632 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2633 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2634 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2635 ) 2636 2637 # --- view "Payments" lines: 2638 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2639 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2640 2641 for key in paymentsKeys: 2642 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2643 2644 info.append(splitLine1) 2645 2646 # --- view "Commissions and taxes" lines: 2647 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2648 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2649 2650 for key in comKeys: 2651 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2652 2653 info.extend([ 2654 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2655 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2656 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2657 ]) 2658 2659 else: 2660 info.append("Broker returned no operations during this period\n") 2661 2662 # --- view "Operations" section: 2663 for item in ops: 2664 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2665 continue 2666 2667 else: 2668 self._figi = item["figi"] 2669 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2670 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2671 2672 # group of deals during one day: 2673 if nextDay and item["date"].split("T")[0] != nextDay: 2674 info.append(splitLine2) 2675 nextDay = "" 2676 2677 else: 2678 nextDay = item["date"].split("T")[0] # saving current day for splitting 2679 2680 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2681 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2682 self._figi if self._figi else "—", 2683 instrument["ticker"] if instrument else "—", 2684 instrument["type"] if instrument else "—", 2685 item["quantity"] if int(item["quantity"]) > 0 else "—", 2686 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2687 TKS_OPERATION_STATES[item["state"]], 2688 TKS_OPERATION_TYPES[item["operationType"]], 2689 )) 2690 2691 infoText = "".join(info) 2692 2693 if show and not onlyFiles: 2694 if self.moreDebug: 2695 uLogger.debug("Records about history of a client's operations successfully received") 2696 2697 uLogger.info(infoText) 2698 2699 if self.reportFile and (show or onlyFiles): 2700 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2701 fH.write(infoText) 2702 2703 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2704 2705 if self.useHTMLReports: 2706 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2707 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2708 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2709 2710 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2711 2712 return ops, customStat 2713 2714 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2715 """ 2716 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2717 2718 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2719 Warning! Broker server used ISO UTC time by default. 2720 2721 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2722 Also, `historyFile` used to update history with `onlyMissing` parameter. 2723 2724 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2725 2726 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2727 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2728 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2729 `"hour"`, `"day"`. Default: `"hour"`. 2730 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2731 False by default. Warning! History appends only from last candle to current time 2732 with always update last candle! 2733 :param csvSep: separator if csv-file is used, `,` by default. 2734 :param show: if `True` then also prints Pandas DataFrame to the console. 2735 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2736 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2737 `["date", "time", "open", "high", "low", "close", "volume"]`. 2738 """ 2739 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2740 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2741 history = None # empty pandas object for history 2742 2743 if interval not in TKS_CANDLE_INTERVALS.keys(): 2744 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2745 raise Exception("Incorrect value") 2746 2747 if not (self._ticker or self._figi): 2748 uLogger.error("Ticker or FIGI must be defined!") 2749 raise Exception("Ticker or FIGI required") 2750 2751 if self._ticker and not self._figi: 2752 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2753 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2754 2755 if self._figi and not self._ticker: 2756 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2757 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2758 2759 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2760 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2761 if interval.lower() != "day": 2762 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2763 2764 delta = dtEnd - dtStart # current UTC time minus last time in file 2765 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2766 2767 # calculate history length in candles: 2768 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2769 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2770 length += 1 # to avoid fraction time 2771 2772 # calculate data blocks count: 2773 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2774 2775 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2776 if self.moreDebug: 2777 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2778 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2779 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2780 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2781 2782 tempOld = None # pandas object for old history, if --only-missing key present 2783 lastTime = None # datetime object of last old candle in file 2784 2785 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2786 if self.moreDebug: 2787 uLogger.debug("--only-missing key present, add only last missing candles...") 2788 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2789 2790 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2791 2792 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2793 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2794 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2795 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2796 2797 # get last datetime object from last string in file or minus 1 delta if file is empty: 2798 if len(tempOld) > 0: 2799 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2800 2801 else: 2802 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2803 2804 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2805 2806 responseJSONs = [] # raw history blocks of data 2807 2808 blockEnd = dtEnd 2809 for item in range(blocks): 2810 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2811 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2812 2813 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2814 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2815 )) 2816 2817 if blockStart == blockEnd: 2818 uLogger.debug("Skipped this zero-length block...") 2819 2820 else: 2821 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2822 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2823 self.body = str({ 2824 "figi": self._figi, 2825 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2826 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2827 "interval": TKS_CANDLE_INTERVALS[interval][0] 2828 }) 2829 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2830 2831 if "code" in responseJSON.keys(): 2832 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2833 2834 else: 2835 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2836 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2837 2838 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2839 2840 blockEnd = blockStart 2841 2842 printCount = len(responseJSONs) # candles to show in console 2843 if responseJSONs: 2844 tempHistory = pd.DataFrame( 2845 data={ 2846 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2847 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2848 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2849 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2850 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2851 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2852 "volume": [int(item["volume"]) for item in responseJSONs], 2853 }, 2854 index=range(len(responseJSONs)), 2855 columns=["date", "time", "open", "high", "low", "close", "volume"], 2856 ) 2857 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2858 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2859 2860 # append only newest candles to old history if --only-missing key present: 2861 if onlyMissing and tempOld is not None and lastTime is not None: 2862 index = 0 # find start index in tempHistory data: 2863 2864 for i, item in tempHistory.iterrows(): 2865 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2866 2867 if curTime == lastTime: 2868 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2869 index = i 2870 printCount = i + 1 2871 break 2872 2873 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2874 2875 else: 2876 history = tempHistory # if no `--only-missing` key then load full data from server 2877 2878 if self.moreDebug: 2879 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2880 2881 if history is not None and not history.empty: 2882 if show and not onlyFiles: 2883 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2884 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2885 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2886 )) 2887 2888 else: 2889 uLogger.warning("Received an empty candles history!") 2890 2891 if self.historyFile is not None: 2892 if history is not None and not history.empty: 2893 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2894 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2895 2896 else: 2897 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2898 2899 else: 2900 if self.moreDebug: 2901 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2902 2903 return history 2904 2905 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2906 """ 2907 Load candles history from csv-file and return Pandas DataFrame object. 2908 2909 See also: `History()` and `ShowHistoryChart()` methods. 2910 2911 :param filePath: path to csv-file to open. 2912 """ 2913 loadedHistory = None # init candles data object 2914 2915 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2916 2917 if os.path.exists(filePath): 2918 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2919 2920 tfStr = self.priceModel.FormattedDelta( 2921 self.priceModel.timeframe, 2922 "{days} days {hours}h {minutes}m {seconds}s", 2923 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2924 self.priceModel.timeframe, 2925 "{hours}h {minutes}m {seconds}s", 2926 ) 2927 2928 if loadedHistory is not None and not loadedHistory.empty: 2929 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2930 len(loadedHistory), 2931 tfStr, 2932 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2933 ) 2934 2935 else: 2936 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2937 2938 else: 2939 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2940 2941 return loadedHistory 2942 2943 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2944 """ 2945 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2946 2947 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2948 Default: `index.html` (both for interact and non-interact candlesticks chart). 2949 2950 See also: `History()` and `LoadHistory()` methods. 2951 2952 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2953 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2954 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2955 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2956 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2957 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2958 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2959 """ 2960 if isinstance(candles, str): 2961 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2962 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2963 2964 elif isinstance(candles, pd.DataFrame): 2965 self.priceModel.prices = candles # set candles chain from variable 2966 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2967 2968 if "datetime" not in candles.columns: 2969 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2970 2971 else: 2972 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2973 raise Exception("Incorrect value") 2974 2975 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2976 2977 if interact: 2978 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2979 2980 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2981 2982 else: 2983 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2984 2985 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2986 2987 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2988 2989 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2990 """ 2991 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2992 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2993 2994 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2995 2996 :param operation: string "Buy" or "Sell". 2997 :param lots: volume, integer count of lots >= 1. 2998 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2999 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3000 :param expDate: string "Undefined" by default or local date in future, 3001 it is a string with format `%Y-%m-%d %H:%M:%S`. 3002 :return: JSON with response from broker server. 3003 """ 3004 if self.accountId is None or not self.accountId: 3005 uLogger.error("Variable `accountId` must be defined for using this method!") 3006 raise Exception("Account ID required") 3007 3008 if operation is None or not operation or operation not in ("Buy", "Sell"): 3009 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3010 raise Exception("Incorrect value") 3011 3012 if lots is None or lots < 1: 3013 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3014 lots = 1 3015 3016 if tp is None or tp < 0: 3017 tp = 0 3018 3019 if sl is None or sl < 0: 3020 sl = 0 3021 3022 if expDate is None or not expDate: 3023 expDate = "Undefined" 3024 3025 if not (self._ticker or self._figi): 3026 uLogger.error("Ticker or FIGI must be defined!") 3027 raise Exception("Ticker or FIGI required") 3028 3029 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3030 self._ticker = instrument["ticker"] 3031 self._figi = instrument["figi"] 3032 3033 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3034 3035 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3036 self.body = str({ 3037 "figi": self._figi, 3038 "quantity": str(lots), 3039 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3040 "accountId": str(self.accountId), 3041 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3042 }) 3043 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3044 3045 if "orderId" in response.keys(): 3046 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3047 operation, response["orderId"], 3048 self._ticker, self._figi, lots, 3049 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3050 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3051 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3052 )) 3053 3054 if tp > 0: 3055 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3056 3057 if sl > 0: 3058 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3059 3060 else: 3061 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3062 3063 return response 3064 3065 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3066 """ 3067 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3068 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3069 3070 See also: `Order()` and `Trade()` docstrings. 3071 3072 :param lots: volume, integer count of lots >= 1. 3073 :param tp: float > 0, take profit price of stop-order. 3074 :param sl: float > 0, stop loss price of stop-order. 3075 :param expDate: it's a local date in future. 3076 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3077 :return: JSON with response from broker server. 3078 """ 3079 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3080 3081 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3082 """ 3083 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3084 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3085 3086 See also: `Order()` and `Trade()` docstrings. 3087 3088 :param lots: volume, integer count of lots >= 1. 3089 :param tp: float > 0, take profit price of stop-order. 3090 :param sl: float > 0, stop loss price of stop-order. 3091 :param expDate: it's a local date in the future. 3092 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3093 :return: JSON with response from broker server. 3094 """ 3095 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3096 3097 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3098 """ 3099 Close position of given instruments. 3100 3101 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3102 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3103 This avoids unnecessary downloading data from the server. 3104 """ 3105 if instruments is None or not instruments: 3106 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3107 raise Exception("Ticker or FIGI required") 3108 3109 if isinstance(instruments, str): 3110 instruments = [instruments] 3111 3112 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3113 if uniqueInstruments: 3114 if portfolio is None or not portfolio: 3115 portfolio = self.Overview(show=False) 3116 3117 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3118 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3119 3120 for self._figi in uniqueInstruments: 3121 if self._figi not in allOpened: 3122 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3123 continue 3124 3125 # search open trade info about instrument by ticker: 3126 instrument = {} 3127 for iType in TKS_INSTRUMENTS: 3128 if instrument: 3129 break 3130 3131 for item in portfolio["stat"][iType]: 3132 if item["figi"] == self._figi: 3133 instrument = item 3134 break 3135 3136 if instrument: 3137 self._ticker = instrument["ticker"] 3138 self._figi = instrument["figi"] 3139 3140 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3141 self._ticker, 3142 self._figi, 3143 int(instrument["volume"]), 3144 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3145 )) 3146 3147 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3148 3149 if tradeLots > 0: 3150 if instrument["blocked"] > 0: 3151 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3152 instrument["blocked"], 3153 self._ticker, 3154 tradeLots, 3155 )) 3156 3157 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3158 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3159 3160 else: 3161 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3162 3163 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3164 """ 3165 Close all positions of given instruments with defined type. 3166 3167 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3168 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3169 This avoids unnecessary downloading data from the server. 3170 """ 3171 if iType not in TKS_INSTRUMENTS: 3172 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3173 3174 else: 3175 if portfolio is None or not portfolio: 3176 portfolio = self.Overview(show=False) 3177 3178 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3179 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3180 3181 if tickers and portfolio: 3182 self.CloseTrades(tickers, portfolio) 3183 3184 else: 3185 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3186 3187 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3188 """ 3189 Universal method to create market or limit orders with all available parameters for current `accountId`. 3190 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3191 3192 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3193 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3194 3195 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3196 then broker immediately open market order as you can do simple --buy or --sell operations! 3197 3198 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3199 When current price will go up or down to target price value then broker opens a limit order. 3200 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3201 3202 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3203 3204 :param operation: string "Buy" or "Sell". 3205 :param orderType: string "Limit" or "Stop". 3206 :param lots: volume, integer count of lots >= 1. 3207 :param targetPrice: target price > 0. This is open trade price for limit order. 3208 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3209 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3210 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3211 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3212 Stop loss order always executed by market price. 3213 :param expDate: string "Undefined" by default or local date in future. 3214 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3215 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3216 A limit order has no expiration date, it lasts until the end of the trading day. 3217 :return: JSON with response from broker server. 3218 """ 3219 if self.accountId is None or not self.accountId: 3220 uLogger.error("Variable `accountId` must be defined for using this method!") 3221 raise Exception("Account ID required") 3222 3223 if operation is None or not operation or operation not in ("Buy", "Sell"): 3224 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3225 raise Exception("Incorrect value") 3226 3227 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3228 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3229 raise Exception("Incorrect value") 3230 3231 if lots is None or lots < 1: 3232 uLogger.error("You must define trade volume > 0: integer count of lots!") 3233 raise Exception("Incorrect value") 3234 3235 if targetPrice is None or targetPrice <= 0: 3236 uLogger.error("Target price for limit-order must be greater than 0!") 3237 raise Exception("Incorrect value") 3238 3239 if limitPrice is None or limitPrice <= 0: 3240 limitPrice = targetPrice 3241 3242 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3243 stopType = "Limit" 3244 3245 if expDate is None or not expDate: 3246 expDate = "Undefined" 3247 3248 if not (self._ticker or self._figi): 3249 uLogger.error("Tocker or FIGI must be defined!") 3250 raise Exception("Ticker or FIGI required") 3251 3252 response = {} 3253 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3254 self._ticker = instrument["ticker"] 3255 self._figi = instrument["figi"] 3256 3257 if orderType == "Limit": 3258 uLogger.debug( 3259 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3260 self._ticker, self._figi, 3261 operation, lots, targetPrice, instrument["currency"], 3262 )) 3263 3264 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3265 self.body = str({ 3266 "figi": self._figi, 3267 "quantity": str(lots), 3268 "price": FloatToNano(targetPrice), 3269 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3270 "accountId": str(self.accountId), 3271 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3272 }) 3273 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3274 3275 if "orderId" in response.keys(): 3276 uLogger.info( 3277 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3278 response["orderId"], self._ticker, self._figi, operation, lots, 3279 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3280 )) 3281 3282 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3283 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3284 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3285 targetPrice, instrument["currency"], 3286 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3287 )) 3288 3289 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3290 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3291 targetPrice, instrument["currency"], 3292 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3293 )) 3294 3295 else: 3296 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3297 3298 if orderType == "Stop": 3299 uLogger.debug( 3300 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3301 self._ticker, self._figi, 3302 operation, lots, 3303 targetPrice, instrument["currency"], 3304 limitPrice, instrument["currency"], 3305 stopType, expDate, 3306 )) 3307 3308 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3309 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3310 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3311 3312 body = { 3313 "figi": self._figi, 3314 "quantity": str(lots), 3315 "price": FloatToNano(limitPrice), 3316 "stopPrice": FloatToNano(targetPrice), 3317 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3318 "accountId": str(self.accountId), 3319 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3320 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3321 } 3322 3323 if expDateUTC: 3324 body["expireDate"] = expDateUTC 3325 3326 self.body = str(body) 3327 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3328 3329 if "stopOrderId" in response.keys(): 3330 uLogger.info( 3331 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3332 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3333 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3334 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3335 TKS_STOP_ORDER_TYPES[stopOrderType], 3336 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3337 )) 3338 3339 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3340 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3341 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3342 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3343 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3344 )) 3345 3346 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3347 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3348 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3349 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3350 )) 3351 3352 else: 3353 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3354 3355 return response 3356 3357 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3358 """ 3359 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3360 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3361 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3362 See also: `Order()` docstring. 3363 3364 :param lots: volume, integer count of lots >= 1. 3365 :param targetPrice: target price > 0. This is open trade price for limit order. 3366 :return: JSON with response from broker server. 3367 """ 3368 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3369 3370 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3371 """ 3372 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3373 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3374 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3375 target price value then broker opens a limit order. See also: `Order()` docstring. 3376 3377 :param lots: volume, integer count of lots >= 1. 3378 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3379 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3380 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3381 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3382 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3383 :param expDate: string "Undefined" by default or local date in future. 3384 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3385 This date is converting to UTC format for server. 3386 :return: JSON with response from broker server. 3387 """ 3388 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3389 3390 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3391 """ 3392 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3393 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3394 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3395 See also: `Order()` docstring. 3396 3397 :param lots: volume, integer count of lots >= 1. 3398 :param targetPrice: target price > 0. This is open trade price for limit order. 3399 :return: JSON with response from broker server. 3400 """ 3401 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3402 3403 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3404 """ 3405 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3406 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3407 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3408 target price value then broker opens a limit order. See also: `Order()` docstring. 3409 3410 :param lots: volume, integer count of lots >= 1. 3411 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3412 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3413 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3414 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3415 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3416 :param expDate: string "Undefined" by default or local date in future. 3417 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3418 This date is converting to UTC format for server. 3419 :return: JSON with response from broker server. 3420 """ 3421 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3422 3423 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3424 """ 3425 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3426 3427 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3428 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3429 This avoids unnecessary downloading data from the server. 3430 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3431 """ 3432 if self.accountId is None or not self.accountId: 3433 uLogger.error("Variable `accountId` must be defined for using this method!") 3434 raise Exception("Account ID required") 3435 3436 if orderIDs: 3437 if allOrdersIDs is None: 3438 rawOrders = self.RequestPendingOrders() 3439 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3440 3441 if allStopOrdersIDs is None: 3442 rawStopOrders = self.RequestStopOrders() 3443 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3444 3445 for orderID in orderIDs: 3446 idInPendingOrders = orderID in allOrdersIDs 3447 idInStopOrders = orderID in allStopOrdersIDs 3448 3449 if not (idInPendingOrders or idInStopOrders): 3450 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3451 continue 3452 3453 else: 3454 if idInPendingOrders: 3455 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3456 3457 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3458 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3459 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3460 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3461 3462 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3463 if self.moreDebug: 3464 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3465 3466 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3467 3468 else: 3469 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3470 3471 elif idInStopOrders: 3472 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3473 3474 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3475 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3476 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3477 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3478 3479 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3480 if self.moreDebug: 3481 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3482 3483 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3484 3485 else: 3486 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3487 3488 else: 3489 continue 3490 3491 def CloseAllOrders(self) -> None: 3492 """ 3493 Gets a list of open pending and stop orders and cancel it all. 3494 """ 3495 rawOrders = self.RequestPendingOrders() 3496 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3497 lenOrders = len(allOrdersIDs) 3498 3499 rawStopOrders = self.RequestStopOrders() 3500 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3501 lenSOrders = len(allStopOrdersIDs) 3502 3503 if lenOrders > 0 or lenSOrders > 0: 3504 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3505 3506 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3507 3508 else: 3509 uLogger.info("Orders not found, nothing to cancel.") 3510 3511 def CloseAll(self, *args) -> None: 3512 """ 3513 Close all available (not blocked) opened trades and orders. 3514 3515 Also, you can select one or more keywords case-insensitive: 3516 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3517 3518 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3519 """ 3520 overview = self.Overview(show=False) # get all open trades info 3521 3522 if len(args) == 0: 3523 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3524 self.CloseAllOrders() # close all pending and stop orders 3525 3526 for iType in TKS_INSTRUMENTS: 3527 if iType != "Currencies": 3528 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3529 3530 else: 3531 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3532 lowerArgs = [x.lower() for x in args] 3533 3534 if "orders" in lowerArgs: 3535 self.CloseAllOrders() # close all pending and stop orders 3536 3537 for iType in TKS_INSTRUMENTS: 3538 if iType.lower() in lowerArgs and iType != "Currencies": 3539 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3540 3541 def CloseAllByTicker(self, instrument: str) -> None: 3542 """ 3543 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3544 3545 This method searches opened trade and orders of instrument throw all portfolio and then use 3546 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3547 3548 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3549 3550 :param instrument: string with ticker. 3551 """ 3552 if instrument is None or not instrument: 3553 uLogger.error("Ticker name must be defined for using this method!") 3554 raise Exception("Ticker required") 3555 3556 overview = self.Overview(show=False) # get user portfolio with all open trades info 3557 3558 self._ticker = instrument # try to set instrument as ticker 3559 self._figi = "" 3560 3561 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3562 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3563 3564 if limitAll and self.IsInLimitOrders(portfolio=overview): 3565 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3566 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3567 3568 if stopAll and self.IsInStopOrders(portfolio=overview): 3569 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3570 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3571 3572 if self.IsInPortfolio(portfolio=overview): 3573 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3574 self.CloseTrades(instruments=[instrument], portfolio=overview) 3575 3576 def CloseAllByFIGI(self, instrument: str) -> None: 3577 """ 3578 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3579 3580 This method searches opened trade and orders of instrument throw all portfolio and then use 3581 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3582 3583 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3584 3585 :param instrument: string with FIGI id. 3586 """ 3587 if instrument is None or not instrument: 3588 uLogger.error("FIGI id must be defined for using this method!") 3589 raise Exception("FIGI required") 3590 3591 overview = self.Overview(show=False) # get user portfolio with all open trades info 3592 3593 self._ticker = "" 3594 self._figi = instrument # try to set instrument as FIGI id 3595 3596 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3597 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3598 3599 if limitAll and self.IsInLimitOrders(portfolio=overview): 3600 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3601 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3602 3603 if stopAll and self.IsInStopOrders(portfolio=overview): 3604 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3605 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3606 3607 if self.IsInPortfolio(portfolio=overview): 3608 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3609 self.CloseTrades(instruments=[instrument], portfolio=overview) 3610 3611 @staticmethod 3612 def ParseOrderParameters(operation, **inputParameters): 3613 """ 3614 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3615 3616 :param operation: string "Buy" or "Sell". 3617 :param inputParameters: this is dict of strings that looks like this 3618 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3619 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3620 "prices" key: one or more prices to open limit-orders 3621 Counts of values in lots and prices lists must be equals! 3622 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3623 """ 3624 # TODO: update order grid work with api v2 3625 pass 3626 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3627 # 3628 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3629 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3630 # raise Exception("Incorrect value") 3631 # 3632 # if "l" in inputParameters.keys(): 3633 # inputParameters["lots"] = inputParameters.pop("l") 3634 # 3635 # if "p" in inputParameters.keys(): 3636 # inputParameters["prices"] = inputParameters.pop("p") 3637 # 3638 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3639 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3640 # raise Exception("Incorrect value") 3641 # 3642 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3643 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3644 # 3645 # if len(lots) != len(prices): 3646 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3647 # raise Exception("Incorrect value") 3648 # 3649 # uLogger.debug("Extracted parameters for orders:") 3650 # uLogger.debug("lots = {}".format(lots)) 3651 # uLogger.debug("prices = {}".format(prices)) 3652 # 3653 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3654 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3655 # uLogger.debug("Order parameters: {}".format(result)) 3656 # 3657 # return result 3658 3659 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3660 """ 3661 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3662 3663 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3664 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3665 """ 3666 result = False 3667 msg = "Instrument not defined!" 3668 3669 if portfolio is None or not portfolio: 3670 portfolio = self.Overview(show=False) 3671 3672 if self._ticker: 3673 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3674 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3675 3676 for iType in TKS_INSTRUMENTS: 3677 for instrument in portfolio["stat"][iType]: 3678 if instrument["ticker"] == self._ticker: 3679 result = True 3680 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3681 break 3682 3683 elif self._figi: 3684 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3685 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3686 3687 for iType in TKS_INSTRUMENTS: 3688 for instrument in portfolio["stat"][iType]: 3689 if instrument["figi"] == self._figi: 3690 result = True 3691 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3692 break 3693 3694 else: 3695 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3696 3697 uLogger.debug(msg) 3698 3699 return result 3700 3701 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3702 """ 3703 Returns instrument from the user's portfolio if it presents there. 3704 Instrument must be defined by `ticker` (highly priority) or `figi`. 3705 3706 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3707 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3708 """ 3709 result = None 3710 msg = "Instrument not defined!" 3711 3712 if portfolio is None or not portfolio: 3713 portfolio = self.Overview(show=False) 3714 3715 if self._ticker: 3716 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3717 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3718 3719 for iType in TKS_INSTRUMENTS: 3720 for instrument in portfolio["stat"][iType]: 3721 if instrument["ticker"] == self._ticker: 3722 result = instrument 3723 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3724 break 3725 3726 elif self._figi: 3727 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3728 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3729 3730 for iType in TKS_INSTRUMENTS: 3731 for instrument in portfolio["stat"][iType]: 3732 if instrument["figi"] == self._figi: 3733 result = instrument 3734 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3735 break 3736 3737 else: 3738 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3739 3740 uLogger.debug(msg) 3741 3742 return result 3743 3744 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3745 """ 3746 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3747 3748 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3749 3750 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3751 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3752 """ 3753 result = False 3754 msg = "Instrument not defined!" 3755 3756 if portfolio is None or not portfolio: 3757 portfolio = self.Overview(show=False) 3758 3759 if self._ticker: 3760 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3761 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3762 3763 for instrument in portfolio["stat"]["orders"]: 3764 if instrument["ticker"] == self._ticker: 3765 result = True 3766 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3767 break 3768 3769 elif self._figi: 3770 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3771 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3772 3773 for instrument in portfolio["stat"]["orders"]: 3774 if instrument["figi"] == self._figi: 3775 result = True 3776 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3777 break 3778 3779 else: 3780 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3781 3782 uLogger.debug(msg) 3783 3784 return result 3785 3786 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3787 """ 3788 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3789 Instrument must be defined by `ticker` (highly priority) or `figi`. 3790 3791 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3792 3793 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3794 :return: list with `orderID`s of limit orders. 3795 """ 3796 result = [] 3797 msg = "Instrument not defined!" 3798 3799 if portfolio is None or not portfolio: 3800 portfolio = self.Overview(show=False) 3801 3802 if self._ticker: 3803 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3804 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3805 3806 for instrument in portfolio["stat"]["orders"]: 3807 if instrument["ticker"] == self._ticker: 3808 result.append(instrument["orderID"]) 3809 3810 if result: 3811 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3812 3813 elif self._figi: 3814 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3815 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3816 3817 for instrument in portfolio["stat"]["orders"]: 3818 if instrument["figi"] == self._figi: 3819 result.append(instrument["orderID"]) 3820 3821 if result: 3822 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3823 3824 else: 3825 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3826 3827 uLogger.debug(msg) 3828 3829 return result 3830 3831 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3832 """ 3833 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3834 3835 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3836 3837 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3838 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3839 """ 3840 result = False 3841 msg = "Instrument not defined!" 3842 3843 if portfolio is None or not portfolio: 3844 portfolio = self.Overview(show=False) 3845 3846 if self._ticker: 3847 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3848 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3849 3850 for instrument in portfolio["stat"]["stopOrders"]: 3851 if instrument["ticker"] == self._ticker: 3852 result = True 3853 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3854 break 3855 3856 elif self._figi: 3857 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3858 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3859 3860 for instrument in portfolio["stat"]["stopOrders"]: 3861 if instrument["figi"] == self._figi: 3862 result = True 3863 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3864 break 3865 3866 else: 3867 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3868 3869 uLogger.debug(msg) 3870 3871 return result 3872 3873 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3874 """ 3875 Returns list with all `orderID`s of opened stop orders for the instrument. 3876 Instrument must be defined by `ticker` (highly priority) or `figi`. 3877 3878 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3879 3880 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3881 :return: list with `orderID`s of stop orders. 3882 """ 3883 result = [] 3884 msg = "Instrument not defined!" 3885 3886 if portfolio is None or not portfolio: 3887 portfolio = self.Overview(show=False) 3888 3889 if self._ticker: 3890 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3891 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3892 3893 for instrument in portfolio["stat"]["stopOrders"]: 3894 if instrument["ticker"] == self._ticker: 3895 result.append(instrument["orderID"]) 3896 3897 if result: 3898 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3899 3900 elif self._figi: 3901 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3902 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3903 3904 for instrument in portfolio["stat"]["stopOrders"]: 3905 if instrument["figi"] == self._figi: 3906 result.append(instrument["orderID"]) 3907 3908 if result: 3909 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3910 3911 else: 3912 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3913 3914 uLogger.debug(msg) 3915 3916 return result 3917 3918 def RequestLimits(self) -> dict: 3919 """ 3920 Method for obtaining the available funds for withdrawal for current `accountId`. 3921 3922 See also: 3923 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3924 - `OverviewLimits()` method 3925 3926 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3927 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3928 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3929 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3930 """ 3931 if self.accountId is None or not self.accountId: 3932 uLogger.error("Variable `accountId` must be defined for using this method!") 3933 raise Exception("Account ID required") 3934 3935 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3936 3937 self.body = str({"accountId": self.accountId}) 3938 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3939 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3940 3941 if self.moreDebug: 3942 uLogger.debug("Records about available funds for withdrawal successfully received") 3943 3944 return rawLimits 3945 3946 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3947 """ 3948 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3949 3950 See also: `RequestLimits()`. 3951 3952 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3953 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3954 :return: dict with raw parsed data from server and some calculated statistics about it. 3955 """ 3956 if self.accountId is None or not self.accountId: 3957 uLogger.error("Variable `accountId` must be defined for using this method!") 3958 raise Exception("Account ID required") 3959 3960 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3961 3962 view = { 3963 "rawLimits": rawLimits, 3964 "limits": { # parsed data for every currency: 3965 "money": { # this is an array of portfolio currency positions 3966 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3967 }, 3968 "blocked": { # this is an array of blocked currency 3969 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3970 }, 3971 "blockedGuarantee": { # this is locked money under collateral for futures 3972 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3973 }, 3974 }, 3975 } 3976 3977 # --- Prepare text table with limits in human-readable format: 3978 if show or onlyFiles: 3979 info = [ 3980 "# Withdrawal limits\n\n", 3981 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3982 "* **Account ID:** [{}]\n".format(self.accountId), 3983 ] 3984 3985 if view["limits"]["money"]: 3986 info.extend([ 3987 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3988 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3989 ]) 3990 3991 else: 3992 info.append("\nNo withdrawal limits\n") 3993 3994 for curr in view["limits"]["money"].keys(): 3995 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3996 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3997 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3998 3999 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4000 "[{}]".format(curr), 4001 "{:.2f}".format(view["limits"]["money"][curr]), 4002 "{:.2f}".format(availableMoney), 4003 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4004 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4005 ) 4006 4007 if curr == "rub": 4008 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4009 4010 else: 4011 info.append(infoStr) 4012 4013 infoText = "".join(info) 4014 4015 if show and not onlyFiles: 4016 uLogger.info(infoText) 4017 4018 if self.withdrawalLimitsFile and (show or onlyFiles): 4019 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4020 fH.write(infoText) 4021 4022 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4023 4024 if self.useHTMLReports: 4025 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4026 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4027 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4028 4029 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4030 4031 return view 4032 4033 def RequestAccounts(self) -> dict: 4034 """ 4035 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4036 4037 See also: 4038 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4039 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4040 - `OverviewUserInfo()` method 4041 4042 :return: dict with raw data from server that contains accounts info. Example of dict: 4043 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4044 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4045 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4046 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4047 """ 4048 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4049 4050 self.body = str({}) 4051 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4052 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4053 4054 if self.moreDebug: 4055 uLogger.debug("Records about available accounts successfully received") 4056 4057 return rawAccounts 4058 4059 def RequestUserInfo(self) -> dict: 4060 """ 4061 Method for requesting common user's information. 4062 4063 See also: 4064 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4065 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4066 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4067 - `OverviewUserInfo()` method 4068 4069 :return: dict with raw data from server that contains user's information. Example of dict: 4070 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4071 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4072 """ 4073 uLogger.debug("Requesting common user's information. Wait, please...") 4074 4075 self.body = str({}) 4076 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4077 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4078 4079 if self.moreDebug: 4080 uLogger.debug("Records about current user successfully received") 4081 4082 return rawUserInfo 4083 4084 def RequestMarginStatus(self, accountId: str = None) -> dict: 4085 """ 4086 Method for requesting margin calculation for defined account ID. 4087 4088 See also: 4089 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4090 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4091 - `OverviewUserInfo()` method 4092 4093 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4094 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4095 Example of responses: 4096 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4097 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4098 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4099 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4100 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4101 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4102 """ 4103 if accountId is None or not accountId: 4104 if self.accountId is None or not self.accountId: 4105 uLogger.error("Variable `accountId` must be defined for using this method!") 4106 raise Exception("Account ID required") 4107 4108 else: 4109 accountId = self.accountId # use `self.accountId` (main ID) by default 4110 4111 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4112 4113 self.body = str({"accountId": accountId}) 4114 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4115 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4116 4117 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4118 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4119 rawMargin = {} 4120 4121 else: 4122 if self.moreDebug: 4123 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4124 4125 return rawMargin 4126 4127 def RequestTariffLimits(self) -> dict: 4128 """ 4129 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4130 4131 See also: 4132 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4133 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4134 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4135 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4136 - `OverviewUserInfo()` method 4137 4138 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4139 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4140 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4141 """ 4142 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4143 4144 self.body = str({}) 4145 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4146 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4147 4148 if self.moreDebug: 4149 uLogger.debug("Records with limits of current tariff successfully received") 4150 4151 return rawTariffLimits 4152 4153 def RequestBondCoupons(self, iJSON: dict) -> dict: 4154 """ 4155 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4156 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4157 All dates are in UTC timezone. 4158 4159 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4160 Documentation: 4161 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4162 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4163 4164 See also: `ExtendBondsData()`. 4165 4166 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4167 If raw iJSON is not data of bond then server returns an error [400] with message: 4168 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4169 :return: dictionary with bond payment calendar. Response example 4170 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4171 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4172 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4173 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4174 """ 4175 if iJSON["figi"] is None or not iJSON["figi"]: 4176 uLogger.error("FIGI must be defined for using this method!") 4177 raise Exception("FIGI required") 4178 4179 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4180 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4181 4182 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4183 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4184 self._figi, 4185 startDate, 4186 endDate, 4187 )) 4188 4189 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4190 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4191 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4192 4193 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4194 uLogger.warning("Instrument type is not bond!") 4195 4196 else: 4197 if self.moreDebug: 4198 uLogger.debug("Records about bond payment calendar successfully received") 4199 4200 return calendar 4201 4202 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4203 """ 4204 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4205 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4206 coupon yields, current yields and some statistics etc. 4207 4208 WARNING! This is too long operation if a lot of bonds requested from broker server. 4209 4210 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4211 4212 :param instruments: list of strings with tickers or FIGIs. 4213 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4214 for further used by data scientists or stock analytics. 4215 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4216 In XLSX-file and Pandas DataFrame fields mean: 4217 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4218 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4219 """ 4220 if instruments is None or not instruments: 4221 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4222 raise Exception("Ticker or FIGI required") 4223 4224 if isinstance(instruments, str): 4225 instruments = [instruments] 4226 4227 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4228 4229 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4230 4231 iCount = len(uniqueInstruments) 4232 tooLong = iCount >= 20 4233 if tooLong: 4234 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4235 4236 bonds = None 4237 for i, self._figi in enumerate(uniqueInstruments): 4238 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4239 4240 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4241 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4242 rawBond = self.SearchByFIGI(requestPrice=True) 4243 4244 # Widen raw data with UTC current time (iData["actualDateTime"]): 4245 actualDate = datetime.now(tzutc()) 4246 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4247 4248 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4249 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4250 4251 # Replace some values with human-readable: 4252 iData["nominalCurrency"] = iData["nominal"]["currency"] 4253 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4254 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4255 iData["aciCurrency"] = iData["aciValue"]["currency"] 4256 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4257 iData["issueSize"] = int(iData["issueSize"]) 4258 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4259 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4260 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4261 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4262 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4263 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4264 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4265 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4266 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4267 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4268 4269 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4270 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4271 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4272 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4273 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4274 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4275 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4276 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4277 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4278 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4279 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4280 4281 # Widen raw data with calendar data from `rawCalendar` values: 4282 calendarData = [] 4283 if "events" in iData["rawCalendar"].keys(): 4284 for item in iData["rawCalendar"]["events"]: 4285 calendarData.append({ 4286 "couponDate": item["couponDate"], 4287 "couponNumber": int(item["couponNumber"]), 4288 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4289 "payCurrency": item["payOneBond"]["currency"], 4290 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4291 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4292 "couponStartDate": item["couponStartDate"], 4293 "couponEndDate": item["couponEndDate"], 4294 "couponPeriod": item["couponPeriod"], 4295 }) 4296 4297 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4298 if "maturityDate" not in iData.keys(): 4299 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4300 4301 # Widen raw data with Coupon Rate. 4302 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4303 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4304 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4305 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4306 4307 # Widen raw data with Yield to Maturity (YTM) on current date. 4308 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4309 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4310 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4311 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4312 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4313 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4314 4315 iData["calendar"] = calendarData # adds calendar at the end 4316 4317 # Remove not used data: 4318 iData.pop("uid") 4319 iData.pop("positionUid") 4320 iData.pop("currentPrice") 4321 iData.pop("rawCalendar") 4322 4323 colNames = list(iData.keys()) 4324 if bonds is None: 4325 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4326 4327 else: 4328 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4329 4330 else: 4331 uLogger.warning("Instrument is not a bond!") 4332 4333 processed = round(100 * (i + 1) / iCount, 1) 4334 if tooLong and processed % 5 == 0: 4335 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4336 4337 else: 4338 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4339 4340 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4341 4342 # Saving bonds from Pandas DataFrame to XLSX sheet: 4343 if xlsx and self.bondsXLSXFile: 4344 with pd.ExcelWriter( 4345 path=self.bondsXLSXFile, 4346 date_format=TKS_DATE_FORMAT, 4347 datetime_format=TKS_DATE_TIME_FORMAT, 4348 mode="w", 4349 ) as writer: 4350 bonds.to_excel( 4351 writer, 4352 sheet_name="Extended bonds data", 4353 index=True, 4354 encoding="UTF-8", 4355 freeze_panes=(1, 1), 4356 ) # saving as XLSX-file with freeze first row and column as headers 4357 4358 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4359 4360 return bonds 4361 4362 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4363 """ 4364 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4365 4366 WARNING! This is too long operation if a lot of bonds requested from broker server. 4367 4368 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4369 4370 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4371 extended information about bonds: main info, current prices, bond payment calendar, 4372 coupon yields, current yields and some statistics etc. 4373 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4374 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4375 for further used by data scientists or stock analytics. 4376 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4377 """ 4378 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4379 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4380 4381 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4382 4383 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4384 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4385 calendar = None 4386 for bond in extBonds.iterrows(): 4387 for item in bond[1]["calendar"]: 4388 cData = { 4389 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4390 "couponDate": item["couponDate"], 4391 "figi": bond[1]["figi"], 4392 "ticker": bond[1]["ticker"], 4393 "name": bond[1]["name"], 4394 "couponNumber": item["couponNumber"], 4395 "payOneBond": item["payOneBond"], 4396 "payCurrency": item["payCurrency"], 4397 "couponType": item["couponType"], 4398 "couponPeriod": item["couponPeriod"], 4399 "fixDate": item["fixDate"], 4400 "couponStartDate": item["couponStartDate"], 4401 "couponEndDate": item["couponEndDate"], 4402 } 4403 4404 if calendar is None: 4405 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4406 4407 else: 4408 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4409 4410 if calendar is not None: 4411 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4412 4413 # Saving calendar from Pandas DataFrame to XLSX sheet: 4414 if xlsx: 4415 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4416 4417 with pd.ExcelWriter( 4418 path=xlsxCalendarFile, 4419 date_format=TKS_DATE_FORMAT, 4420 datetime_format=TKS_DATE_TIME_FORMAT, 4421 mode="w", 4422 ) as writer: 4423 humanReadable = calendar.copy(deep=True) 4424 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4425 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4426 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4427 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4428 humanReadable.columns = colNames # human-readable column names 4429 4430 humanReadable.to_excel( 4431 writer, 4432 sheet_name="Bond payments calendar", 4433 index=False, 4434 encoding="UTF-8", 4435 freeze_panes=(1, 2), 4436 ) # saving as XLSX-file with freeze first row and column as headers 4437 4438 del humanReadable # release df in memory 4439 4440 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4441 4442 return calendar 4443 4444 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4445 """ 4446 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4447 Also, creates Markdown file with calendar data, `calendar.md` by default. 4448 4449 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4450 4451 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4452 extended information about bonds: main info, current prices, bond payment calendar, 4453 coupon yields, current yields and some statistics etc. 4454 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4455 :param show: if `True` then also printing bonds payment calendar to the console, 4456 otherwise save to file `calendarFile` only. `False` by default. 4457 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4458 :return: multilines text in Markdown format with bonds payment calendar as a table. 4459 """ 4460 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4461 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4462 4463 infoText = "# Bond payments calendar\n\n" 4464 4465 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4466 4467 if not (calendar is None or calendar.empty): 4468 splitLine = "| | | | | | | | | |\n" 4469 4470 info = [ 4471 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4472 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4473 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4474 ] 4475 4476 newMonth = False 4477 notOneBond = calendar["figi"].nunique() > 1 4478 for i, bond in enumerate(calendar.iterrows()): 4479 if newMonth and notOneBond: 4480 info.append(splitLine) 4481 4482 info.append( 4483 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4484 " √" if bond[1]["paid"] else " —", 4485 bond[1]["couponDate"].split("T")[0], 4486 bond[1]["figi"], 4487 bond[1]["ticker"], 4488 bond[1]["couponNumber"], 4489 "{} {}".format( 4490 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4491 bond[1]["payCurrency"], 4492 ), 4493 bond[1]["couponType"], 4494 bond[1]["couponPeriod"], 4495 bond[1]["fixDate"].split("T")[0], 4496 ) 4497 ) 4498 4499 if i < len(calendar.values) - 1: 4500 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4501 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4502 newMonth = False if curDate.month == nextDate.month else True 4503 4504 else: 4505 newMonth = False 4506 4507 infoText += "".join(info) 4508 4509 if show and not onlyFiles: 4510 uLogger.info("{}".format(infoText)) 4511 4512 if self.calendarFile is not None and (show or onlyFiles): 4513 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4514 fH.write(infoText) 4515 4516 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4517 4518 if self.useHTMLReports: 4519 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4520 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4521 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4522 4523 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4524 4525 else: 4526 infoText += "No data\n" 4527 4528 return infoText 4529 4530 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4531 """ 4532 Method for parsing and show simple table with all available user accounts. 4533 4534 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4535 4536 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4537 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4538 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4539 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4540 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4541 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4542 "closed": "—", "access": "Full access" }, ...}}` 4543 """ 4544 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4545 4546 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4547 accounts = { 4548 item["id"]: { 4549 "type": TKS_ACCOUNT_TYPES[item["type"]], 4550 "name": item["name"], 4551 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4552 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4553 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4554 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4555 } for item in rawAccounts["accounts"] 4556 } 4557 4558 # Raw and parsed data with some fields replaced in "stat" section: 4559 view = { 4560 "rawAccounts": rawAccounts, 4561 "stat": accounts, 4562 } 4563 4564 # --- Prepare simple text table with only accounts data in human-readable format: 4565 if show or onlyFiles: 4566 info = [ 4567 "# User accounts\n\n", 4568 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4569 "| Account ID | Type | Status | Name |\n", 4570 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4571 ] 4572 4573 for account in view["stat"].keys(): 4574 info.extend([ 4575 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4576 account, 4577 view["stat"][account]["type"], 4578 view["stat"][account]["status"], 4579 view["stat"][account]["name"], 4580 ) 4581 ]) 4582 4583 infoText = "".join(info) 4584 4585 if show and not onlyFiles: 4586 uLogger.info(infoText) 4587 4588 if self.userAccountsFile and (show or onlyFiles): 4589 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4590 fH.write(infoText) 4591 4592 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4593 4594 if self.useHTMLReports: 4595 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4596 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4597 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4598 4599 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4600 4601 return view 4602 4603 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4604 """ 4605 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4606 4607 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4608 4609 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4610 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4611 :return: dict with raw parsed data from server and some calculated statistics about it. 4612 """ 4613 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4614 tmpTicker = self._ticker 4615 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4616 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4617 self._ticker = tmpTicker 4618 4619 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4620 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4621 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4622 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4623 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4624 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4625 4626 # This is dict with parsed common user data: 4627 userInfo = { 4628 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4629 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4630 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4631 "tariff": rawUserInfo["tariff"], 4632 } 4633 4634 # This is an array of dict with parsed margin statuses for every account IDs: 4635 margins = {} 4636 for accountId in accounts.keys(): 4637 if rawMargins[accountId]: 4638 margins[accountId] = { 4639 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4640 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4641 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4642 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4643 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4644 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4645 "missing": missing["volume"], 4646 } 4647 4648 else: 4649 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4650 4651 unary = {} # unary-connection limits 4652 for item in rawTariffLimits["unaryLimits"]: 4653 if item["limitPerMinute"] in unary.keys(): 4654 unary[item["limitPerMinute"]].extend(item["methods"]) 4655 4656 else: 4657 unary[item["limitPerMinute"]] = item["methods"] 4658 4659 stream = {} # stream-connection limits 4660 for item in rawTariffLimits["streamLimits"]: 4661 if item["limit"] in stream.keys(): 4662 stream[item["limit"]].extend(item["streams"]) 4663 4664 else: 4665 stream[item["limit"]] = item["streams"] 4666 4667 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4668 limits = { 4669 "unary": unary, 4670 "stream": stream, 4671 } 4672 4673 # Raw and parsed data as an output result: 4674 view = { 4675 "rawUserInfo": rawUserInfo, 4676 "rawAccounts": rawAccounts, 4677 "rawMargins": rawMargins, 4678 "rawTariffLimits": rawTariffLimits, 4679 "stat": { 4680 "overview": overview, 4681 "userInfo": userInfo, 4682 "accounts": accounts, 4683 "margins": margins, 4684 "limits": limits, 4685 }, 4686 } 4687 4688 # --- Prepare text table with user information in human-readable format: 4689 if show or onlyFiles: 4690 info = [ 4691 "# Full user information\n\n", 4692 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4693 "## Common information\n\n", 4694 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4695 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4696 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4697 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4698 "\n## User accounts\n\n", 4699 ] 4700 4701 for account in view["stat"]["accounts"].keys(): 4702 info.extend([ 4703 "### ID: [{}]\n\n".format(account), 4704 "| Parameters | Values |\n", 4705 "|----------------------|--------------------------------------------------------------|\n", 4706 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4707 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4708 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4709 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4710 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4711 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4712 ]) 4713 4714 if margins[account]: 4715 info.extend([ 4716 "| Margin status: | Enabled |\n", 4717 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4718 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4719 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4720 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4721 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4722 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4723 ]) 4724 4725 else: 4726 info.append("| Margin status: | Disabled |\n\n") 4727 4728 info.extend([ 4729 "\n## Current user tariff limits\n", 4730 "\n### See also\n", 4731 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4732 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4733 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4734 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4735 "\n### Unary limits\n", 4736 ]) 4737 4738 if unary: 4739 for key, values in sorted(unary.items()): 4740 info.append("\n* Max requests per minute: {}\n".format(key)) 4741 4742 for value in values: 4743 info.append(" - {}\n".format(value)) 4744 4745 else: 4746 info.append("\nNot available\n") 4747 4748 info.append("\n### Stream limits\n") 4749 4750 if stream: 4751 for key, values in sorted(stream.items()): 4752 info.append("\n* Max stream connections: {}\n".format(key)) 4753 4754 for value in values: 4755 info.append(" - {}\n".format(value)) 4756 4757 else: 4758 info.append("\nNot available\n") 4759 4760 infoText = "".join(info) 4761 4762 if show and not onlyFiles: 4763 uLogger.info(infoText) 4764 4765 if self.userInfoFile and (show or onlyFiles): 4766 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4767 fH.write(infoText) 4768 4769 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4770 4771 if self.useHTMLReports: 4772 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4773 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4774 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4775 4776 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4777 4778 return view 4779 4780 4781class Args: 4782 """ 4783 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4784 """ 4785 def __init__(self, **kwargs): 4786 self.__dict__.update(kwargs) 4787 4788 def __getattr__(self, item): 4789 return None 4790 4791 4792def ParseArgs(): 4793 """This function get and parse command line keys.""" 4794 parser = ArgumentParser() # command-line string parser 4795 4796 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4797 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4798 4799 # --- options: 4800 4801 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4802 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4803 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4804 4805 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4806 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4807 4808 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4809 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4810 4811 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4812 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4813 4814 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4815 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4816 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4817 4818 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4819 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4820 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4821 4822 # --- commands: 4823 4824 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4825 4826 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4827 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4828 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4829 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4830 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4831 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4832 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4833 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4834 4835 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4836 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4837 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4838 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4839 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4840 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4841 4842 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4843 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4844 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4845 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4846 4847 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4848 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4849 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4850 4851 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4852 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4853 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4854 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4855 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4856 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4857 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4858 4859 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4860 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4861 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4862 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4863 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4864 4865 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4866 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4867 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4868 4869 cmdArgs = parser.parse_args() 4870 return cmdArgs 4871 4872 4873def Main(**kwargs): 4874 """ 4875 Main function for work with TKSBrokerAPI in the console. 4876 4877 See examples: 4878 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4879 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4880 """ 4881 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4882 4883 if args.debug_level: 4884 uLogger.level = 10 # always debug level by default 4885 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4886 4887 exitCode = 0 4888 start = datetime.now(tzutc()) 4889 uLogger.debug("=-" * 50) 4890 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4891 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4892 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4893 )) 4894 4895 # trying to calculate full current version: 4896 buildVersion = __version__ 4897 try: 4898 v = version("tksbrokerapi") 4899 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4900 4901 except Exception: 4902 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4903 4904 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4905 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4906 4907 try: 4908 if args.version: 4909 print("TKSBrokerAPI {}".format(buildVersion)) 4910 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4911 4912 else: 4913 # Init class for trading with Tinkoff Broker: 4914 trader = TinkoffBrokerServer( 4915 token=args.token, 4916 accountId=args.account_id, 4917 useCache=not args.no_cache, 4918 ) 4919 4920 if args.tag is not None: 4921 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4922 4923 # --- set some options: 4924 4925 if args.more: 4926 trader.moreDebug = True 4927 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4928 4929 if args.html: 4930 trader.useHTMLReports = True 4931 4932 if args.ticker: 4933 ticker = str(args.ticker).upper() # Tickers may be upper case only 4934 4935 if ticker in trader.aliasesKeys: 4936 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4937 4938 else: 4939 trader.ticker = ticker 4940 4941 if args.figi: 4942 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4943 4944 if args.depth is not None: 4945 trader.depth = args.depth 4946 4947 # --- do one command: 4948 4949 if args.list: 4950 if args.output is not None: 4951 trader.instrumentsFile = args.output 4952 4953 trader.ShowInstrumentsInfo(show=True) 4954 4955 elif args.list_xlsx: 4956 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4957 4958 elif args.bonds_xlsx is not None: 4959 if args.output is not None: 4960 trader.bondsXLSXFile = args.output 4961 4962 if len(args.bonds_xlsx) == 0: 4963 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4964 4965 else: 4966 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4967 4968 elif args.search: 4969 if args.output is not None: 4970 trader.searchResultsFile = args.output 4971 4972 trader.SearchInstruments(pattern=args.search[0], show=True) 4973 4974 elif args.info: 4975 if not (args.ticker or args.figi): 4976 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4977 raise Exception("Ticker or FIGI required") 4978 4979 if args.output is not None: 4980 trader.infoFile = args.output 4981 4982 if args.ticker: 4983 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4984 4985 else: 4986 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4987 4988 elif args.calendar is not None: 4989 if args.output is not None: 4990 trader.calendarFile = args.output 4991 4992 if len(args.calendar) == 0: 4993 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4994 4995 else: 4996 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4997 4998 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4999 5000 elif args.price: 5001 if not (args.ticker or args.figi): 5002 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5003 raise Exception("Ticker or FIGI required") 5004 5005 trader.GetCurrentPrices(show=True) 5006 5007 elif args.prices is not None: 5008 if args.output is not None: 5009 trader.pricesFile = args.output 5010 5011 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5012 5013 elif args.overview: 5014 if args.output is not None: 5015 trader.overviewFile = args.output 5016 5017 trader.Overview(show=True, details="full") 5018 5019 elif args.overview_digest: 5020 if args.output is not None: 5021 trader.overviewDigestFile = args.output 5022 5023 trader.Overview(show=True, details="digest") 5024 5025 elif args.overview_positions: 5026 if args.output is not None: 5027 trader.overviewPositionsFile = args.output 5028 5029 trader.Overview(show=True, details="positions") 5030 5031 elif args.overview_orders: 5032 if args.output is not None: 5033 trader.overviewOrdersFile = args.output 5034 5035 trader.Overview(show=True, details="orders") 5036 5037 elif args.overview_analytics: 5038 if args.output is not None: 5039 trader.overviewAnalyticsFile = args.output 5040 5041 trader.Overview(show=True, details="analytics") 5042 5043 elif args.overview_calendar: 5044 if args.output is not None: 5045 trader.overviewAnalyticsFile = args.output 5046 5047 trader.Overview(show=True, details="calendar") 5048 5049 elif args.deals is not None: 5050 if args.output is not None: 5051 trader.reportFile = args.output 5052 5053 if 0 <= len(args.deals) < 3: 5054 trader.Deals( 5055 start=args.deals[0] if len(args.deals) >= 1 else None, 5056 end=args.deals[1] if len(args.deals) == 2 else None, 5057 show=True, # Always show deals report in console 5058 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5059 ) 5060 5061 else: 5062 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5063 raise Exception("Incorrect value") 5064 5065 elif args.history is not None: 5066 if args.output is not None: 5067 trader.historyFile = args.output 5068 5069 if 0 <= len(args.history) < 3: 5070 dataReceived = trader.History( 5071 start=args.history[0] if len(args.history) >= 1 else None, 5072 end=args.history[1] if len(args.history) == 2 else None, 5073 interval="hour" if args.interval is None or not args.interval else args.interval, 5074 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5075 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5076 show=True, # shows all downloaded candles in console 5077 ) 5078 5079 if args.render_chart is not None and dataReceived is not None: 5080 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5081 5082 trader.ShowHistoryChart( 5083 candles=dataReceived, 5084 interact=iChart, 5085 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5086 ) 5087 5088 else: 5089 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5090 raise Exception("Incorrect value") 5091 5092 elif args.load_history is not None: 5093 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5094 5095 if args.render_chart is not None and histData is not None: 5096 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5097 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5098 5099 trader.ShowHistoryChart( 5100 candles=histData, 5101 interact=iChart, 5102 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5103 ) 5104 5105 elif args.trade is not None: 5106 if 1 <= len(args.trade) <= 5: 5107 trader.Trade( 5108 operation=args.trade[0], 5109 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5110 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5111 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5112 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5113 ) 5114 5115 else: 5116 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5117 5118 elif args.buy is not None: 5119 if 0 <= len(args.buy) <= 4: 5120 trader.Buy( 5121 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5122 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5123 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5124 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5125 ) 5126 5127 else: 5128 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5129 5130 elif args.sell is not None: 5131 if 0 <= len(args.sell) <= 4: 5132 trader.Sell( 5133 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5134 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5135 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5136 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5137 ) 5138 5139 else: 5140 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5141 5142 elif args.order: 5143 if 4 <= len(args.order) <= 7: 5144 trader.Order( 5145 operation=args.order[0], 5146 orderType=args.order[1], 5147 lots=int(args.order[2]), 5148 targetPrice=float(args.order[3]), 5149 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5150 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5151 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5152 ) 5153 5154 else: 5155 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5156 5157 elif args.buy_limit: 5158 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5159 5160 elif args.sell_limit: 5161 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5162 5163 elif args.buy_stop: 5164 if 2 <= len(args.buy_stop) <= 7: 5165 trader.BuyStop( 5166 lots=int(args.buy_stop[0]), 5167 targetPrice=float(args.buy_stop[1]), 5168 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5169 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5170 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5171 ) 5172 5173 else: 5174 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5175 5176 elif args.sell_stop: 5177 if 2 <= len(args.sell_stop) <= 7: 5178 trader.SellStop( 5179 lots=int(args.sell_stop[0]), 5180 targetPrice=float(args.sell_stop[1]), 5181 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5182 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5183 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5184 ) 5185 5186 else: 5187 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5188 5189 # elif args.buy_order_grid is not None: 5190 # # update order grid work with api v2 5191 # if len(args.buy_order_grid) == 2: 5192 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5193 # 5194 # for order in orderParams: 5195 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5196 # 5197 # else: 5198 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5199 # 5200 # elif args.sell_order_grid is not None: 5201 # # update order grid work with api v2 5202 # if len(args.sell_order_grid) >= 2: 5203 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5204 # 5205 # for order in orderParams: 5206 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5207 # 5208 # else: 5209 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5210 5211 elif args.close_order is not None: 5212 trader.CloseOrders(args.close_order) # close only one order 5213 5214 elif args.close_orders is not None: 5215 trader.CloseOrders(args.close_orders) # close list of orders 5216 5217 elif args.close_trade: 5218 if not (args.ticker or args.figi): 5219 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5220 raise Exception("Ticker or FIGI required") 5221 5222 if args.ticker: 5223 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5224 5225 else: 5226 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5227 5228 elif args.close_trades is not None: 5229 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5230 5231 elif args.close_all is not None: 5232 if args.ticker: 5233 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5234 5235 elif args.figi: 5236 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5237 5238 else: 5239 trader.CloseAll(*args.close_all) 5240 5241 elif args.limits: 5242 if args.output is not None: 5243 trader.withdrawalLimitsFile = args.output 5244 5245 trader.OverviewLimits(show=True) 5246 5247 elif args.user_info: 5248 if args.output is not None: 5249 trader.userInfoFile = args.output 5250 5251 trader.OverviewUserInfo(show=True) 5252 5253 elif args.account: 5254 if args.output is not None: 5255 trader.userAccountsFile = args.output 5256 5257 trader.OverviewAccounts(show=True) 5258 5259 else: 5260 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5261 raise Exception("There is no command to execute") 5262 5263 except Exception: 5264 trace = tb.format_exc() 5265 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5266 if e in trace: 5267 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5268 break 5269 5270 uLogger.debug(trace) 5271 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5272 exitCode = 255 # an error occurred, must be open a ticket for this issue 5273 5274 finally: 5275 finish = datetime.now(tzutc()) 5276 5277 if exitCode == 0: 5278 if args.more: 5279 uLogger.debug("All operations were finished success (summary code is 0).") 5280 5281 else: 5282 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5283 os.path.abspath(uLog.defaultLogFile), exitCode, 5284 )) 5285 5286 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5287 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5288 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5289 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5290 )) 5291 uLogger.debug("=-" * 50) 5292 5293 if not kwargs: 5294 sys.exit(exitCode) 5295 5296 else: 5297 return exitCode 5298 5299 5300if __name__ == "__main__": 5301 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self._tag = "" 130 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 131 132 self.__lock = Lock() # initialize multiprocessing mutex lock 133 134 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 135 136 self.aliases = TKS_TICKER_ALIASES 137 """Some aliases instead official tickers. 138 139 See also: `TKSEnums.TKS_TICKER_ALIASES` 140 """ 141 142 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 143 144 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 145 146 self._ticker = "" 147 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 148 149 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 150 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 151 152 See also: `SearchByTicker()`, `SearchInstruments()`. 153 """ 154 155 self._figi = "" 156 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 157 158 See also: `SearchByFIGI()`, `SearchInstruments()`. 159 """ 160 161 self.depth = 1 162 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 163 164 See also: `GetCurrentPrices()`. 165 """ 166 167 self.server = r"https://invest-public-api.tinkoff.ru/rest" 168 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 169 170 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 171 """ 172 173 uLogger.debug("Broker API server: {}".format(self.server)) 174 175 self.timeout = 15 176 """Server operations timeout in seconds. Default: `15`. 177 178 See also: `SendAPIRequest()`. 179 """ 180 181 self.headers = { 182 "Content-Type": "application/json", 183 "accept": "application/json", 184 "Authorization": "Bearer {}".format(self.token), 185 "x-app-name": "Tim55667757.TKSBrokerAPI", 186 } 187 """ 188 Headers which send in every request to broker server. Please, do not change it! 189 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 190 191 See also: `SendAPIRequest()`. 192 """ 193 194 self.body = None 195 """Request body which send to broker server. Default: `None`. 196 197 See also: `SendAPIRequest()`. 198 """ 199 200 self.moreDebug = False 201 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 202 203 self.useHTMLReports = False 204 """ 205 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 206 207 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 208 """ 209 210 self.historyFile = None 211 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 212 213 See also: `History()`. 214 """ 215 216 self.htmlHistoryFile = "index.html" 217 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 218 219 See also: `ShowHistoryChart()`. 220 """ 221 222 self.instrumentsFile = "instruments.md" 223 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 224 225 See also: `ShowInstrumentsInfo()`. 226 """ 227 228 self.searchResultsFile = "search-results.md" 229 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 230 231 See also: `SearchInstruments()`. 232 """ 233 234 self.pricesFile = "prices.md" 235 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 236 237 See also: `GetListOfPrices()`. 238 """ 239 240 self.infoFile = "info.md" 241 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 242 243 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 244 """ 245 246 self.bondsXLSXFile = "ext-bonds.xlsx" 247 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 248 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 249 250 See also: `ExtendBondsData()`. 251 """ 252 253 self.calendarFile = "calendar.md" 254 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 255 256 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 257 258 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 259 """ 260 261 self.overviewFile = "overview.md" 262 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 263 264 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 265 """ 266 267 self.overviewDigestFile = "overview-digest.md" 268 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 269 270 See also: `Overview()` with parameter `details="digest"`. 271 """ 272 273 self.overviewPositionsFile = "overview-positions.md" 274 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 275 276 See also: `Overview()` with parameter `details="positions"`. 277 """ 278 279 self.overviewOrdersFile = "overview-orders.md" 280 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 281 282 See also: `Overview()` with parameter `details="orders"`. 283 """ 284 285 self.overviewAnalyticsFile = "overview-analytics.md" 286 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 287 288 See also: `Overview()` with parameter `details="analytics"`. 289 """ 290 291 self.overviewBondsCalendarFile = "overview-calendar.md" 292 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 293 294 See also: `Overview()` with parameter `details="calendar"`. 295 """ 296 297 self.reportFile = "deals.md" 298 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 299 300 See also: `Deals()`. 301 """ 302 303 self.withdrawalLimitsFile = "limits.md" 304 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 305 306 See also: `OverviewLimits()` and `RequestLimits()`. 307 """ 308 309 self.userInfoFile = "user-info.md" 310 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 311 312 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 313 """ 314 315 self.userAccountsFile = "accounts.md" 316 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 317 318 See also: `OverviewAccounts()`, `RequestAccounts()`. 319 """ 320 321 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 322 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 323 324 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 325 326 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 327 """ 328 329 self.iList = None # init iList for raw instruments data 330 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 331 332 See also: `Listing()`, `DumpInstruments()`. 333 """ 334 335 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 336 if useCache: 337 if os.path.exists(self.iListDumpFile): 338 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 339 curTime = datetime.now(tzutc()) 340 341 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 342 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 343 344 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 345 346 else: 347 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 348 349 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 350 os.path.abspath(self.iListDumpFile), 351 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 352 )) 353 354 else: 355 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 356 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 357 358 else: 359 self.iList = self.Listing() # request new raw instruments data from broker server 360 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 361 362 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 363 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 364 365 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 366 """ 367 368 @property 369 def tag(self) -> str: 370 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 371 return self._tag 372 373 @tag.setter 374 def tag(self, value): 375 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 376 self._tag = str(value) 377 378 if self._tag: 379 for handler in uLogger.handlers: 380 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 381 382 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 383 384 else: 385 for handler in uLogger.handlers: 386 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 387 388 uLogger.debug("Default logger format is used") 389 390 @property 391 def ticker(self) -> str: 392 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 393 394 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 395 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 396 397 See also: `SearchByTicker()`, `SearchInstruments()`. 398 """ 399 return self._ticker 400 401 @ticker.setter 402 def ticker(self, value): 403 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 404 405 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 406 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 407 408 See also: `SearchByTicker()`, `SearchInstruments()`. 409 """ 410 self._ticker = str(value).upper() # Tickers may be upper case only 411 412 @property 413 def figi(self) -> str: 414 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 415 416 See also: `SearchByFIGI()`, `SearchInstruments()`. 417 """ 418 return self._figi 419 420 @figi.setter 421 def figi(self, value): 422 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 423 424 See also: `SearchByFIGI()`, `SearchInstruments()`. 425 """ 426 self._figi = str(value).upper() # FIGI may be upper case only 427 428 def _ParseJSON(self, rawData="{}") -> dict: 429 """ 430 Parse JSON from response string. 431 432 :param rawData: this is a string with JSON-formatted text. 433 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 434 """ 435 try: 436 responseJSON = json.loads(rawData) if rawData else {} 437 438 if self.moreDebug: 439 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 440 441 return responseJSON 442 443 except Exception as e: 444 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 445 446 return {} 447 448 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 449 """ 450 Send GET or POST request to broker server and receive JSON object. 451 452 self.header: must be defining with dictionary of headers. 453 self.body: if define then used as request body. None by default. 454 self.timeout: global request timeout, 15 seconds by default. 455 :param url: url with REST request. 456 :param reqType: send "GET" or "POST" request. "GET" by default. 457 :param retry: how many times retry after first request if an 5xx server errors occurred. 458 :param pause: sleep time in seconds between retries. 459 :return: response JSON (dictionary) from broker. 460 """ 461 if reqType.upper() not in ("GET", "POST"): 462 uLogger.error("You can define request type: `GET` or `POST`!") 463 raise Exception("Incorrect value") 464 465 if self.moreDebug: 466 uLogger.debug("Request parameters:") 467 uLogger.debug(" - REST API URL: {}".format(url)) 468 uLogger.debug(" - request type: {}".format(reqType)) 469 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 470 uLogger.debug(" - body:\n{}".format(self.body)) 471 472 # fast hack to avoid all operations with some tickers/FIGI 473 responseJSON = {} 474 oK = True 475 for item in self.exclude: 476 if item in url: 477 if self.moreDebug: 478 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 479 480 oK = False 481 break 482 483 if oK: 484 with self.__lock: # acquire the mutex lock 485 counter = 0 486 response = None 487 errMsg = "" 488 489 while not response and counter <= retry: 490 if reqType == "GET": 491 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 492 493 if reqType == "POST": 494 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 495 496 if self.moreDebug: 497 uLogger.debug("Response:") 498 uLogger.debug(" - status code: {}".format(response.status_code)) 499 uLogger.debug(" - reason: {}".format(response.reason)) 500 uLogger.debug(" - body length: {}".format(len(response.text))) 501 uLogger.debug(" - headers:\n{}".format(response.headers)) 502 503 # Server returns some headers: 504 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 505 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 506 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 507 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 508 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 509 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 510 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 511 sleep(rateLimitWait) 512 513 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 514 if 400 <= response.status_code < 500: 515 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 516 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 517 518 if "code" in response.text and "message" in response.text: 519 msgDict = self._ParseJSON(rawData=response.text) 520 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 521 522 counter = retry + 1 # do not retry for 4xx errors 523 524 if 500 <= response.status_code < 600: 525 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 526 uLogger.debug(" - not oK, {}".format(errMsg)) 527 528 if "code" in response.text and "message" in response.text: 529 errMsgDict = self._ParseJSON(rawData=response.text) 530 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 531 532 counter += 1 533 534 if counter <= retry: 535 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 536 sleep(pause) 537 538 responseJSON = self._ParseJSON(rawData=response.text) 539 540 if errMsg: 541 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 542 uLogger.error(" - not oK, {}".format(errMsg)) 543 544 return responseJSON 545 546 def _IUpdater(self, iType: str) -> tuple: 547 """ 548 Request instrument by type from server. See available API methods for instruments: 549 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 550 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 551 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 552 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 553 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 554 555 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 556 :return: tuple with iType name and list of available instruments of current type for defined user token. 557 """ 558 result = [] 559 560 if iType in TKS_INSTRUMENTS: 561 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 562 563 # all instruments have the same body in API v2 requests: 564 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 565 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 566 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 567 568 return iType, result 569 570 def _IWrapper(self, kwargs): 571 """ 572 Wrapper runs instrument's update method `_IUpdater()`. 573 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 574 """ 575 return self._IUpdater(**kwargs) 576 577 def Listing(self) -> dict: 578 """ 579 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 580 581 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 582 """ 583 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 584 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 585 586 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 587 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 588 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 589 590 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 591 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 592 poolUpdater.close() # close the thread pool 593 poolUpdater.join() # wait a moment until all data returns from threads 594 595 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 596 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 597 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 598 599 # calculate minimum price increment (step) for all instruments and set up instrument's type: 600 for iType in iList.keys(): 601 for ticker in iList[iType]: 602 iList[iType][ticker]["type"] = iType 603 604 if "minPriceIncrement" in iList[iType][ticker].keys(): 605 iList[iType][ticker]["step"] = NanoToFloat( 606 iList[iType][ticker]["minPriceIncrement"]["units"], 607 iList[iType][ticker]["minPriceIncrement"]["nano"], 608 ) 609 610 else: 611 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 612 613 return iList 614 615 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 616 """ 617 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 618 619 See also: `DumpInstruments()`, `Listing()`. 620 621 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 622 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 623 """ 624 if self.iListDumpFile is None or not self.iListDumpFile: 625 uLogger.error("Output name of dump file must be defined!") 626 raise Exception("Filename required") 627 628 if not self.iList or forceUpdate: 629 self.iList = self.Listing() 630 631 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 632 633 # Save as XLSX with separated sheets for every type of instruments: 634 with pd.ExcelWriter( 635 path=xlsxDumpFile, 636 date_format=TKS_DATE_FORMAT, 637 datetime_format=TKS_DATE_TIME_FORMAT, 638 mode="w", 639 ) as writer: 640 for iType in TKS_INSTRUMENTS: 641 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 642 df = df[sorted(df)] # sorted by column names 643 df = df.applymap( 644 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 645 na_action="ignore", 646 ) # converting numbers from nano-type to float in every cell 647 df.to_excel( 648 writer, 649 sheet_name=iType, 650 encoding="UTF-8", 651 freeze_panes=(1, 1), 652 ) # saving as XLSX-file with freeze first row and column as headers 653 654 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 655 656 def DumpInstruments(self, forceUpdate: bool = True) -> str: 657 """ 658 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 659 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 660 661 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 662 663 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 664 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 665 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 666 """ 667 if self.iListDumpFile is None or not self.iListDumpFile: 668 uLogger.error("Output name of dump file must be defined!") 669 raise Exception("Filename required") 670 671 if not self.iList or forceUpdate: 672 self.iList = self.Listing() 673 674 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 675 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 676 fH.write(jsonDump) 677 678 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 679 680 return jsonDump 681 682 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 683 """ 684 Show information about one instrument defined by json data and prints it in Markdown format. 685 686 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 687 688 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 689 :param show: if `True` then also printing information about instrument and its current price. 690 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 691 :return: multilines text in Markdown format with information about one instrument. 692 """ 693 splitLine = "| | |\n" 694 infoText = "" 695 696 if iJSON is not None and iJSON and isinstance(iJSON, dict): 697 info = [ 698 "# Main information\n\n", 699 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 700 "| Parameters | Values |\n", 701 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 702 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 703 "| Full name: | {:<54} |\n".format(iJSON["name"]), 704 ] 705 706 if "sector" in iJSON.keys() and iJSON["sector"]: 707 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 708 709 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 710 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 711 712 info.extend([ 713 splitLine, 714 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 715 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 716 ]) 717 718 if "isin" in iJSON.keys() and iJSON["isin"]: 719 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 720 721 if "classCode" in iJSON.keys(): 722 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 723 724 info.extend([ 725 splitLine, 726 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 727 splitLine, 728 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 729 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 730 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 731 ]) 732 733 if iJSON["figi"]: 734 self._figi = iJSON["figi"] 735 iJSON = iJSON | self.RequestTradingStatus() 736 737 info.extend([ 738 splitLine, 739 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 740 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 741 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 742 ]) 743 744 info.append(splitLine) 745 746 if "type" in iJSON.keys() and iJSON["type"]: 747 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 748 749 if "shareType" in iJSON.keys() and iJSON["shareType"]: 750 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 751 752 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 753 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 754 755 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 756 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 757 758 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 759 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 760 761 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 762 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 763 764 if "focusType" in iJSON.keys() and iJSON["focusType"]: 765 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 766 767 if "assetType" in iJSON.keys() and iJSON["assetType"]: 768 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 769 770 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 771 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 772 773 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 774 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 775 776 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 777 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 778 779 if "currency" in iJSON.keys(): 780 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 781 782 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 783 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 784 785 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 786 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 787 788 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 789 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 790 791 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 792 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 793 794 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 795 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 796 797 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 798 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 799 800 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 801 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 802 803 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 804 info.append("| Perpetual bond: | Yes |\n") 805 806 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 807 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 808 809 iExt = None 810 if iJSON["type"] == "Bonds": 811 info.extend([ 812 splitLine, 813 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 814 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 815 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 816 iJSON["nominal"]["currency"], 817 )), 818 ]) 819 820 if "floatingCouponFlag" in iJSON.keys(): 821 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 822 823 if "amortizationFlag" in iJSON.keys(): 824 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 825 826 info.append(splitLine) 827 828 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 829 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 830 831 if iJSON["figi"]: 832 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 833 834 info.extend([ 835 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 836 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 837 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 838 ]) 839 840 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 841 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 842 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 843 iJSON["aciValue"]["currency"] 844 ))) 845 846 if "currentPrice" in iJSON.keys(): 847 info.append(splitLine) 848 849 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 850 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 851 852 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 853 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 854 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 855 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 856 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 857 858 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 859 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 860 861 info.extend([ 862 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 863 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 864 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 865 )), 866 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 867 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 868 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 869 )), 870 "| Changes between last deal price and last close | {:<54} |\n".format( 871 "{:.2f}%{}".format( 872 iJSON["currentPrice"]["changes"], 873 " ({}{:.2f} {})".format( 874 "+" if bondChangesDelta > 0 else "", 875 bondChangesDelta, 876 aciCurrency 877 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 878 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 879 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 880 currency 881 ), 882 ) 883 ), 884 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 885 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 886 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 887 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 888 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 889 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 890 )), 891 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 895 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 ]) 899 900 if "lot" in iJSON.keys(): 901 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 902 903 if "step" in iJSON.keys() and iJSON["step"] != 0: 904 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 905 906 # Add bond payment calendar: 907 if iJSON["type"] == "Bonds": 908 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 909 info.extend(["\n#", strCalendar]) 910 911 infoText += "".join(info) 912 913 if show and not onlyFiles: 914 uLogger.info("{}".format(infoText)) 915 916 if self.infoFile is not None and (show or onlyFiles): 917 with open(self.infoFile, "w", encoding="UTF-8") as fH: 918 fH.write(infoText) 919 920 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 921 922 if self.useHTMLReports: 923 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 924 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 925 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 926 927 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 928 929 return infoText 930 931 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 932 """ 933 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 934 935 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 936 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 937 :return: JSON formatted data with information about instrument. 938 """ 939 tickerJSON = {} 940 if self.moreDebug: 941 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 942 943 if not self._ticker: 944 uLogger.warning("self._ticker variable is not be empty!") 945 946 else: 947 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 948 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 949 raise Exception("Instrument not allowed") 950 951 if not self.iList: 952 self.iList = self.Listing() 953 954 if self._ticker in self.iList["Shares"].keys(): 955 tickerJSON = self.iList["Shares"][self._ticker] 956 if self.moreDebug: 957 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 958 959 elif self._ticker in self.iList["Currencies"].keys(): 960 tickerJSON = self.iList["Currencies"][self._ticker] 961 if self.moreDebug: 962 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 963 964 elif self._ticker in self.iList["Bonds"].keys(): 965 tickerJSON = self.iList["Bonds"][self._ticker] 966 if self.moreDebug: 967 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 968 969 elif self._ticker in self.iList["Etfs"].keys(): 970 tickerJSON = self.iList["Etfs"][self._ticker] 971 if self.moreDebug: 972 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 973 974 elif self._ticker in self.iList["Futures"].keys(): 975 tickerJSON = self.iList["Futures"][self._ticker] 976 if self.moreDebug: 977 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 978 979 if tickerJSON: 980 self._figi = tickerJSON["figi"] 981 982 if requestPrice: 983 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 984 985 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 986 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 987 988 else: 989 tickerJSON["currentPrice"]["changes"] = 0 990 991 if show: 992 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 993 994 else: 995 if show: 996 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 997 998 return tickerJSON 999 1000 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1001 """ 1002 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1003 1004 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1005 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1006 :return: JSON formatted data with information about instrument. 1007 """ 1008 figiJSON = {} 1009 if self.moreDebug: 1010 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1011 1012 if not self._figi: 1013 uLogger.warning("self._figi variable is not be empty!") 1014 1015 else: 1016 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1017 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1018 raise Exception("Instrument not allowed") 1019 1020 if not self.iList: 1021 self.iList = self.Listing() 1022 1023 for item in self.iList["Shares"].keys(): 1024 if self._figi == self.iList["Shares"][item]["figi"]: 1025 figiJSON = self.iList["Shares"][item] 1026 1027 if self.moreDebug: 1028 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1029 1030 break 1031 1032 if not figiJSON: 1033 for item in self.iList["Currencies"].keys(): 1034 if self._figi == self.iList["Currencies"][item]["figi"]: 1035 figiJSON = self.iList["Currencies"][item] 1036 1037 if self.moreDebug: 1038 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Bonds"].keys(): 1044 if self._figi == self.iList["Bonds"][item]["figi"]: 1045 figiJSON = self.iList["Bonds"][item] 1046 1047 if self.moreDebug: 1048 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Etfs"].keys(): 1054 if self._figi == self.iList["Etfs"][item]["figi"]: 1055 figiJSON = self.iList["Etfs"][item] 1056 1057 if self.moreDebug: 1058 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Futures"].keys(): 1064 if self._figi == self.iList["Futures"][item]["figi"]: 1065 figiJSON = self.iList["Futures"][item] 1066 1067 if self.moreDebug: 1068 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1069 1070 break 1071 1072 if figiJSON: 1073 self._figi = figiJSON["figi"] 1074 self._ticker = figiJSON["ticker"] 1075 1076 if requestPrice: 1077 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1078 1079 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1080 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1081 1082 else: 1083 figiJSON["currentPrice"]["changes"] = 0 1084 1085 if show: 1086 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1087 1088 else: 1089 if show: 1090 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1091 1092 return figiJSON 1093 1094 def GetCurrentPrices(self, show: bool = True) -> dict: 1095 """ 1096 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1097 `{"buy": [{"price": 1243.8, "quantity": 193}, 1098 {"price": 1244.0, "quantity": 168}, 1099 {"price": 1244.8, "quantity": 5}, 1100 {"price": 1245.0, "quantity": 61}, 1101 {"price": 1245.4, "quantity": 60}], 1102 "sell": [{"price": 1243.6, "quantity": 8}, 1103 {"price": 1242.6, "quantity": 10}, 1104 {"price": 1242.4, "quantity": 18}, 1105 {"price": 1242.2, "quantity": 50}, 1106 {"price": 1242.0, "quantity": 113}], 1107 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1108 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1109 - sell: list of dicts with Buyers prices, 1110 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1111 - quantity: volume value by current price in lots, 1112 - limitUp: current trade session limit price, maximum, 1113 - limitDown: current trade session limit price, minimum, 1114 - lastPrice: last deal price of the instrument, 1115 - closePrice: previous trade session close price of the instrument. 1116 1117 See also: `SearchByTicker()` and `SearchByFIGI()`. 1118 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1120 1121 :param show: if `True` then print DOM to log and console. 1122 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1123 If an error occurred then returns an empty record: 1124 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1125 """ 1126 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1127 1128 if self.depth < 1: 1129 uLogger.error("Depth of Market (DOM) must be >=1!") 1130 raise Exception("Incorrect value") 1131 1132 if not (self._ticker or self._figi): 1133 uLogger.error("self._ticker or self._figi variables must be defined!") 1134 raise Exception("Ticker or FIGI required") 1135 1136 if self._ticker and not self._figi: 1137 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1138 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1139 1140 if not self._ticker and self._figi: 1141 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1142 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1143 1144 if not self._figi: 1145 uLogger.error("FIGI is not defined!") 1146 raise Exception("Ticker or FIGI required") 1147 1148 else: 1149 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1150 1151 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1152 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1153 self.body = str({"figi": self._figi, "depth": self.depth}) 1154 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1155 1156 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1157 # list of dicts with sellers orders: 1158 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1159 1160 # list of dicts with buyers orders: 1161 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1162 1163 # max price of instrument at this time: 1164 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1165 1166 # min price of instrument at this time: 1167 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1168 1169 # last price of deal with instrument: 1170 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1171 1172 # last close price of instrument: 1173 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1174 1175 else: 1176 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1177 uLogger.debug("Server response: {}".format(pricesResponse)) 1178 1179 if show: 1180 if prices["buy"] or prices["sell"]: 1181 info = [ 1182 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1183 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1184 self._ticker, 1185 self._figi, 1186 self.depth, 1187 ), 1188 "-" * 60, "\n", 1189 " Orders of Buyers | Orders of Sellers\n", 1190 "-" * 60, "\n", 1191 " Sell prices (volumes) | Buy prices (volumes)\n", 1192 "-" * 60, "\n", 1193 ] 1194 1195 if not prices["buy"]: 1196 info.append(" | No orders!\n") 1197 sumBuy = 0 1198 1199 else: 1200 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1201 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1202 for item in maxMinSorted: 1203 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1204 1205 if not prices["sell"]: 1206 info.append("No orders! |\n") 1207 sumSell = 0 1208 1209 else: 1210 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1211 for item in prices["sell"]: 1212 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1213 1214 info.extend([ 1215 "-" * 60, "\n", 1216 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1217 "-" * 60, "\n", 1218 ]) 1219 1220 infoText = "".join(info) 1221 1222 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1223 1224 else: 1225 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1226 1227 return prices 1228 1229 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1230 """ 1231 This method get and show information about all available broker instruments for current user account. 1232 If `instrumentsFile` string is not empty then also save information to this file. 1233 1234 :param show: if `True` then print results to console, if `False` — print only to file. 1235 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1236 :return: multi-lines string with all available broker instruments. 1237 """ 1238 if not self.iList: 1239 self.iList = self.Listing() 1240 1241 info = [ 1242 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1243 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1244 ] 1245 1246 # add instruments count by type: 1247 for iType in self.iList.keys(): 1248 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1249 1250 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1251 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1252 1253 # generating info tables with all instruments by type: 1254 for iType in self.iList.keys(): 1255 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1256 1257 for instrument in self.iList[iType].keys(): 1258 iName = self.iList[iType][instrument]["name"] # instrument's name 1259 if len(iName) > 57: 1260 iName = "{}...".format(iName[:54]) # right trim for a long string 1261 1262 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1263 self.iList[iType][instrument]["ticker"], 1264 iName, 1265 self.iList[iType][instrument]["figi"], 1266 self.iList[iType][instrument]["currency"], 1267 self.iList[iType][instrument]["lot"], 1268 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1269 )) 1270 1271 infoText = "".join(info) 1272 1273 if show and not onlyFiles: 1274 uLogger.info(infoText) 1275 1276 if self.instrumentsFile and (show or onlyFiles): 1277 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1278 fH.write(infoText) 1279 1280 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1281 1282 if self.useHTMLReports: 1283 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1284 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1285 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1286 1287 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1288 1289 return infoText 1290 1291 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1292 """ 1293 This method search and show information about instruments by part of its ticker, FIGI or name. 1294 If `searchResultsFile` string is not empty then also save information to this file. 1295 1296 :param pattern: string with part of ticker, FIGI or instrument's name. 1297 :param show: if `True` then print results to console, if `False` — return list of result only. 1298 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show and not onlyFiles: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile and (show or onlyFiles): 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 if self.useHTMLReports: 1371 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1372 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1373 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1374 1375 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1376 1377 return searchResults 1378 1379 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1380 """ 1381 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1382 1383 :param instruments: list of strings with tickers or FIGIs. 1384 :return: list with unique instrument FIGIs only. 1385 """ 1386 requestedInstruments = [] 1387 for iName in instruments: 1388 if iName not in self.aliases.keys(): 1389 if iName not in requestedInstruments: 1390 requestedInstruments.append(iName) 1391 1392 else: 1393 if iName not in requestedInstruments: 1394 if self.aliases[iName] not in requestedInstruments: 1395 requestedInstruments.append(self.aliases[iName]) 1396 1397 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1398 1399 onlyUniqueFIGIs = [] 1400 for iName in requestedInstruments: 1401 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1402 continue 1403 1404 self._ticker = iName 1405 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1406 1407 if not iData: 1408 self._ticker = "" 1409 self._figi = iName 1410 1411 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1412 1413 if not iData: 1414 self._figi = "" 1415 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1416 1417 if iData and iData["figi"] not in onlyUniqueFIGIs: 1418 onlyUniqueFIGIs.append(iData["figi"]) 1419 1420 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1421 1422 return onlyUniqueFIGIs 1423 1424 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1425 """ 1426 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1427 1428 See limits: https://tinkoff.github.io/investAPI/limits/ 1429 1430 If `pricesFile` string is not empty then also save information to this file. 1431 1432 :param instruments: list of strings with tickers or FIGIs. 1433 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1434 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1435 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1436 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1437 """ 1438 if instruments is None or not instruments: 1439 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1440 raise Exception("Ticker or FIGI required") 1441 1442 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1443 1444 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1445 1446 iList = [] # trying to get info and current prices about all unique instruments: 1447 for self._figi in onlyUniqueFIGIs: 1448 iData = self.SearchByFIGI(requestPrice=True, show=False) 1449 iList.append(iData) 1450 1451 self.ShowListOfPrices(iList, show, onlyFiles) 1452 1453 return iList 1454 1455 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1456 """ 1457 Show table contains current prices of given instruments. 1458 1459 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1460 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1461 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1462 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1463 :return: multilines text in Markdown format as a table contains current prices. 1464 """ 1465 infoText = "" 1466 1467 if show or self.pricesFile or onlyFiles: 1468 info = [ 1469 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1470 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1471 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1472 ] 1473 1474 for item in iList: 1475 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1476 item["ticker"], 1477 item["figi"], 1478 item["type"], 1479 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1480 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1481 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1482 "{} / {}".format( 1483 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1484 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1485 ), 1486 "{} / {}".format( 1487 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1488 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1489 ), 1490 item["currency"], 1491 )) 1492 1493 infoText = "".join(info) 1494 1495 if show and not onlyFiles: 1496 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1497 1498 if self.pricesFile and (show or onlyFiles): 1499 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1500 fH.write(infoText) 1501 1502 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1503 1504 if self.useHTMLReports: 1505 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1506 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1507 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1508 1509 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1510 1511 return infoText 1512 1513 def RequestTradingStatus(self) -> dict: 1514 """ 1515 Requesting trading status for the instrument defined by `figi` variable. 1516 1517 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1518 1519 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1520 1521 :return: dictionary with trading status attributes. Response example: 1522 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1523 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1524 """ 1525 if self._figi is None or not self._figi: 1526 uLogger.error("Variable `figi` must be defined for using this method!") 1527 raise Exception("FIGI required") 1528 1529 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1530 1531 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1532 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1533 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1534 1535 if self.moreDebug: 1536 uLogger.debug("Records about current trading status successfully received") 1537 1538 return tradingStatus 1539 1540 def RequestPortfolio(self) -> dict: 1541 """ 1542 Requesting actual user's portfolio for current `accountId`. 1543 1544 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1545 1546 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1547 1548 :return: dictionary with user's portfolio. 1549 """ 1550 if self.accountId is None or not self.accountId: 1551 uLogger.error("Variable `accountId` must be defined for using this method!") 1552 raise Exception("Account ID required") 1553 1554 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1555 1556 self.body = str({"accountId": self.accountId}) 1557 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1558 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1559 1560 if self.moreDebug: 1561 uLogger.debug("Records about user's portfolio successfully received") 1562 1563 return rawPortfolio 1564 1565 def RequestPositions(self) -> dict: 1566 """ 1567 Requesting open positions by currencies and instruments for current `accountId`. 1568 1569 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1570 1571 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1572 1573 :return: dictionary with open positions by instruments. 1574 """ 1575 if self.accountId is None or not self.accountId: 1576 uLogger.error("Variable `accountId` must be defined for using this method!") 1577 raise Exception("Account ID required") 1578 1579 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1580 1581 self.body = str({"accountId": self.accountId}) 1582 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1583 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1584 1585 if self.moreDebug: 1586 uLogger.debug("Records about current open positions successfully received") 1587 1588 return rawPositions 1589 1590 def RequestPendingOrders(self) -> list: 1591 """ 1592 Requesting current actual pending limit orders for current `accountId`. 1593 1594 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1595 1596 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1597 1598 :return: list of dictionaries with pending limit orders. 1599 """ 1600 if self.accountId is None or not self.accountId: 1601 uLogger.error("Variable `accountId` must be defined for using this method!") 1602 raise Exception("Account ID required") 1603 1604 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1605 1606 self.body = str({"accountId": self.accountId}) 1607 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1608 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1609 1610 if "orders" in rawResponse.keys(): 1611 rawOrders = rawResponse["orders"] 1612 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1613 1614 else: 1615 rawOrders = [] 1616 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1617 1618 return rawOrders 1619 1620 def RequestStopOrders(self) -> list: 1621 """ 1622 Requesting current actual stop orders for current `accountId`. 1623 1624 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1625 1626 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1627 1628 :return: list of dictionaries with stop orders. 1629 """ 1630 if self.accountId is None or not self.accountId: 1631 uLogger.error("Variable `accountId` must be defined for using this method!") 1632 raise Exception("Account ID required") 1633 1634 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1635 1636 self.body = str({"accountId": self.accountId}) 1637 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1638 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1639 1640 if "stopOrders" in rawResponse.keys(): 1641 rawStopOrders = rawResponse["stopOrders"] 1642 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1643 1644 else: 1645 rawStopOrders = [] 1646 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1647 1648 return rawStopOrders 1649 1650 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1651 """ 1652 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1653 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1654 and `overviewBondsCalendarFile` are defined then also save information to file. 1655 1656 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1657 many requests about the state of the portfolio, and then, based on the received data, a large number 1658 of calculation and statistics are collected. 1659 1660 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1661 :param details: how detailed should the information be? 1662 - `full` — shows full available information about portfolio status (by default), 1663 - `positions` — shows only open positions, 1664 - `orders` — shows only sections of open limits and stop orders. 1665 - `digest` — show a short digest of the portfolio status, 1666 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1667 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1668 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1669 :return: dictionary with client's raw portfolio and some statistics. 1670 """ 1671 if self.accountId is None or not self.accountId: 1672 uLogger.error("Variable `accountId` must be defined for using this method!") 1673 raise Exception("Account ID required") 1674 1675 view = { 1676 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1677 "headers": {}, # list of dictionaries, response headers without "positions" section 1678 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1679 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1680 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1681 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1682 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1683 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1684 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1685 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1686 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1687 }, 1688 "stat": { # --- some statistics calculated using "raw" sections: 1689 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1690 "availableRUB": 0., # available rubles (without other currencies) 1691 "blockedRUB": 0., # blocked sum in Russian Rouble 1692 "totalChangesRUB": 0., # changes for all open trades in RUB 1693 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1694 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1695 "sharesCostRUB": 0., # costs of all shares in RUB 1696 "bondsCostRUB": 0., # costs of all bonds in RUB 1697 "etfsCostRUB": 0., # costs of all etfs in RUB 1698 "futuresCostRUB": 0., # costs of all futures in RUB 1699 "Currencies": [], # list of dictionaries of all currencies statistics 1700 "Shares": [], # list of dictionaries of all shares statistics 1701 "Bonds": [], # list of dictionaries of all bonds statistics 1702 "Etfs": [], # list of dictionaries of all etfs statistics 1703 "Futures": [], # list of dictionaries of all futures statistics 1704 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1705 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1706 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1707 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1708 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1709 }, 1710 "analytics": { # --- some analytics of portfolio: 1711 "distrByAssets": {}, # portfolio distribution by assets 1712 "distrByCompanies": {}, # portfolio distribution by companies 1713 "distrBySectors": {}, # portfolio distribution by sectors 1714 "distrByCurrencies": {}, # portfolio distribution by currencies 1715 "distrByCountries": {}, # portfolio distribution by countries 1716 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1717 } 1718 } 1719 1720 details = details.lower() 1721 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1722 if details not in availableDetails: 1723 details = "full" 1724 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1725 1726 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1727 1728 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1729 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1730 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1731 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1732 1733 # save response headers without "positions" section: 1734 for key in portfolioResponse.keys(): 1735 if key != "positions": 1736 view["raw"]["headers"][key] = portfolioResponse[key] 1737 1738 else: 1739 continue 1740 1741 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1742 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1743 for item in portfolioResponse["positions"]: 1744 if item["instrumentType"] == "currency": 1745 self._figi = item["figi"] 1746 if not self._figi and item["ticker"]: 1747 self._ticker = item["ticker"] 1748 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1749 1750 curr = self.SearchByFIGI(requestPrice=False) 1751 1752 # current price of currency in RUB: 1753 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1754 "name": curr["name"], 1755 "currentPrice": NanoToFloat( 1756 item["currentPrice"]["units"], 1757 item["currentPrice"]["nano"] 1758 ), 1759 } 1760 1761 view["raw"]["Currencies"].append(item) 1762 1763 elif item["instrumentType"] == "share": 1764 view["raw"]["Shares"].append(item) 1765 1766 elif item["instrumentType"] == "bond": 1767 view["raw"]["Bonds"].append(item) 1768 1769 elif item["instrumentType"] == "etf": 1770 view["raw"]["Etfs"].append(item) 1771 1772 elif item["instrumentType"] == "futures": 1773 view["raw"]["Futures"].append(item) 1774 1775 else: 1776 continue 1777 1778 # how many volume of currencies (by ISO currency name) are blocked: 1779 for item in view["raw"]["positions"]["blocked"]: 1780 blocked = NanoToFloat(item["units"], item["nano"]) 1781 if blocked > 0: 1782 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1783 1784 # how many volume of instruments (by FIGI) are blocked: 1785 for item in view["raw"]["positions"]["securities"]: 1786 blocked = int(item["blocked"]) 1787 if blocked > 0: 1788 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1789 1790 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1791 1792 if "rub" in allBlocked.keys(): 1793 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1794 1795 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1796 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1797 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1798 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1799 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1800 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1801 view["stat"]["portfolioCostRUB"] = sum([ 1802 view["stat"]["allCurrenciesCostRUB"], 1803 view["stat"]["sharesCostRUB"], 1804 view["stat"]["bondsCostRUB"], 1805 view["stat"]["etfsCostRUB"], 1806 view["stat"]["futuresCostRUB"], 1807 ]) 1808 1809 # --- calculating some portfolio statistics: 1810 byComp = {} # distribution by companies 1811 bySect = {} # distribution by sectors 1812 byCurr = {} # distribution by currencies (include RUB) 1813 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1814 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1815 1816 for item in portfolioResponse["positions"]: 1817 self._figi = item["figi"] 1818 if not self._figi and item["ticker"]: 1819 self._ticker = item["ticker"] 1820 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1821 1822 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1823 1824 if instrument: 1825 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1826 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1827 1828 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1829 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1830 1831 else: 1832 blocked = 0 1833 1834 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1835 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1836 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1837 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1838 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1839 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1840 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1841 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1842 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1843 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1844 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1845 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1846 1847 statData = { 1848 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1849 "ticker": instrument["ticker"], # ticker by FIGI 1850 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1851 "volume": volume, # available volume of instrument 1852 "lots": lots, # volume in lots of instrument 1853 "direction": direction, # direction of an instrument's position: short or long 1854 "blocked": blocked, # blocked volume of currency or instrument 1855 "currentPrice": curPrice, # current instrument's price in basic asset 1856 "average": average, # current average position price 1857 "cost": cost, # current cost of all volume of instrument in basic asset 1858 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1859 "costRUB": costRUB, # cost of instrument in ruble 1860 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1861 "profit": profit, # expected profit at current moment 1862 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1863 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1864 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1865 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1866 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1867 "step": instrument["step"], # minimum price increment 1868 } 1869 1870 # adding distribution by unique countries: 1871 if statData["country"] not in byCountry.keys(): 1872 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1873 1874 else: 1875 byCountry[statData["country"]]["cost"] += costRUB 1876 byCountry[statData["country"]]["percent"] += percentCostRUB 1877 1878 if item["instrumentType"] != "currency": 1879 # adding distribution by unique companies: 1880 if statData["name"]: 1881 if statData["name"] not in byComp.keys(): 1882 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1883 1884 else: 1885 byComp[statData["name"]]["cost"] += costRUB 1886 byComp[statData["name"]]["percent"] += percentCostRUB 1887 1888 # adding distribution by unique sectors: 1889 if statData["sector"] not in bySect.keys(): 1890 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1891 1892 else: 1893 bySect[statData["sector"]]["cost"] += costRUB 1894 bySect[statData["sector"]]["percent"] += percentCostRUB 1895 1896 # adding distribution by unique currencies: 1897 if currency not in byCurr.keys(): 1898 byCurr[currency] = { 1899 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1900 "cost": costRUB, 1901 "percent": percentCostRUB 1902 } 1903 1904 else: 1905 byCurr[currency]["cost"] += costRUB 1906 byCurr[currency]["percent"] += percentCostRUB 1907 1908 # saving statistics for every instrument: 1909 if item["instrumentType"] == "currency": 1910 view["stat"]["Currencies"].append(statData) 1911 1912 # update dict with free funds for trading (total - blocked) by currencies 1913 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1914 view["stat"]["funds"][currency] = { 1915 "total": volume, 1916 "totalCostRUB": costRUB, # total volume cost in rubles 1917 "free": volume - blocked, 1918 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1919 } 1920 1921 elif item["instrumentType"] == "share": 1922 view["stat"]["Shares"].append(statData) 1923 1924 elif item["instrumentType"] == "bond": 1925 view["stat"]["Bonds"].append(statData) 1926 1927 elif item["instrumentType"] == "etf": 1928 view["stat"]["Etfs"].append(statData) 1929 1930 elif item["instrumentType"] == "Futures": 1931 view["stat"]["Futures"].append(statData) 1932 1933 else: 1934 continue 1935 1936 # total changes in Russian Ruble: 1937 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1938 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1939 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1940 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1941 view["stat"]["funds"]["rub"] = { 1942 "total": view["stat"]["availableRUB"], 1943 "totalCostRUB": view["stat"]["availableRUB"], 1944 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1945 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1946 } 1947 1948 # --- pending limit orders sector data: 1949 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1950 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1951 1952 for item in view["raw"]["orders"]: 1953 self._figi = item["figi"] 1954 1955 if item["figi"] not in uniquePendingOrdersFIGIs: 1956 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1957 1958 uniquePendingOrdersFIGIs.append(item["figi"]) 1959 uniquePendingOrders[item["figi"]] = instrument 1960 1961 else: 1962 instrument = uniquePendingOrders[item["figi"]] 1963 1964 if instrument: 1965 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1966 orderType = TKS_ORDER_TYPES[item["orderType"]] 1967 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1968 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1969 1970 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1971 if item["direction"] == "ORDER_DIRECTION_BUY": 1972 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1973 1974 else: 1975 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1976 1977 # requested price for order execution: 1978 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1979 1980 # necessary changes in percent to reach target from current price: 1981 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1982 1983 view["stat"]["orders"].append({ 1984 "orderID": item["orderId"], # orderId number parameter of current order 1985 "figi": item["figi"], # FIGI identification 1986 "ticker": instrument["ticker"], # ticker name by FIGI 1987 "lotsRequested": item["lotsRequested"], # requested lots value 1988 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for order execution in base currency 1991 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1992 "percentChanges": changes, # changes in percent to target from current price 1993 "currency": item["currency"], # instrument's currency name 1994 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1995 "type": orderType, # type of order from TKS_ORDER_TYPES 1996 "status": orderState, # order status from TKS_ORDER_STATES 1997 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1998 }) 1999 2000 # --- stop orders sector data: 2001 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2002 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2003 2004 for item in view["raw"]["stopOrders"]: 2005 self._figi = item["figi"] 2006 2007 if item["figi"] not in uniqueStopOrdersFIGIs: 2008 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2009 2010 uniqueStopOrdersFIGIs.append(item["figi"]) 2011 uniqueStopOrders[item["figi"]] = instrument 2012 2013 else: 2014 instrument = uniqueStopOrders[item["figi"]] 2015 2016 if instrument: 2017 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2018 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2019 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2020 2021 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2022 if "expirationTime" in item.keys(): 2023 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2024 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2025 2026 else: 2027 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2028 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2029 2030 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2031 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2032 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2033 2034 else: 2035 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2036 2037 # requested price when stop-order executed: 2038 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2039 2040 # price for limit-order, set up when stop-order executed: 2041 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2042 2043 # necessary changes in percent to reach target from current price: 2044 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2045 2046 view["stat"]["stopOrders"].append({ 2047 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2048 "figi": item["figi"], # FIGI identification 2049 "ticker": instrument["ticker"], # ticker name by FIGI 2050 "lotsRequested": item["lotsRequested"], # requested lots value 2051 "currentPrice": lastPrice, # current instrument's price for defined action 2052 "targetPrice": target, # requested price for stop-order execution in base currency 2053 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2054 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2055 "percentChanges": changes, # changes in percent to target from current price 2056 "currency": item["currency"], # instrument's currency name 2057 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2058 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2059 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2060 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2061 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2062 }) 2063 2064 # --- calculating data for analytics section: 2065 # portfolio distribution by assets: 2066 view["analytics"]["distrByAssets"] = { 2067 "Ruble": { 2068 "uniques": 1, 2069 "cost": view["stat"]["availableRUB"], 2070 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2071 }, 2072 "Currencies": { 2073 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2074 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2075 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2076 }, 2077 "Shares": { 2078 "uniques": len(view["stat"]["Shares"]), 2079 "cost": view["stat"]["sharesCostRUB"], 2080 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2081 }, 2082 "Bonds": { 2083 "uniques": len(view["stat"]["Bonds"]), 2084 "cost": view["stat"]["bondsCostRUB"], 2085 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2086 }, 2087 "Etfs": { 2088 "uniques": len(view["stat"]["Etfs"]), 2089 "cost": view["stat"]["etfsCostRUB"], 2090 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2091 }, 2092 "Futures": { 2093 "uniques": len(view["stat"]["Futures"]), 2094 "cost": view["stat"]["futuresCostRUB"], 2095 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2096 }, 2097 } 2098 2099 # portfolio distribution by companies: 2100 view["analytics"]["distrByCompanies"]["All money cash"] = { 2101 "ticker": "", 2102 "cost": view["stat"]["allCurrenciesCostRUB"], 2103 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2104 } 2105 view["analytics"]["distrByCompanies"].update(byComp) 2106 2107 # portfolio distribution by sectors: 2108 view["analytics"]["distrBySectors"]["All money cash"] = { 2109 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2110 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2111 } 2112 view["analytics"]["distrBySectors"].update(bySect) 2113 2114 # portfolio distribution by currencies: 2115 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2116 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2117 2118 if self.moreDebug: 2119 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2120 2121 view["analytics"]["distrByCurrencies"].update(byCurr) 2122 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2123 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2124 2125 # portfolio distribution by countries: 2126 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2127 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2128 2129 if self.moreDebug: 2130 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2131 2132 view["analytics"]["distrByCountries"].update(byCountry) 2133 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2134 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2135 2136 # --- Prepare text statistics overview in human-readable: 2137 if show or onlyFiles: 2138 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2139 2140 # Whatever the value `details`, header not changes: 2141 info = [ 2142 "# Client's portfolio\n\n", 2143 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2144 "* **Account ID:** [{}]\n".format(self.accountId), 2145 ] 2146 2147 if details in ["full", "positions", "digest"]: 2148 info.extend([ 2149 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2150 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2151 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2152 view["stat"]["totalChangesRUB"], 2153 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2154 view["stat"]["totalChangesPercentRUB"], 2155 ), 2156 ]) 2157 2158 if details in ["full", "positions"]: 2159 info.extend([ 2160 "## Open positions\n\n", 2161 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2162 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2163 "| **Ruble:** | {:>31} | | | | | |\n".format( 2164 "{:.2f} ({:.2f}) rub".format( 2165 view["stat"]["availableRUB"], 2166 view["stat"]["blockedRUB"], 2167 ) 2168 ) 2169 ]) 2170 2171 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2172 return [ 2173 "| | | | | | | |\n", 2174 "| {:<27} | | | | | {:>19} | |\n".format( 2175 noTradeStr if noTradeStr else typeStr, 2176 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2177 ), 2178 ] 2179 2180 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2181 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2182 "{} [{}]".format(data["ticker"], data["figi"]), 2183 "{:.2f} ({:.2f}) {}".format( 2184 data["volume"], 2185 data["blocked"], 2186 data["currency"], 2187 ) if isCurr else "{:.0f} ({:.0f})".format( 2188 data["volume"], 2189 data["blocked"], 2190 ), 2191 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2192 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2193 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2194 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2195 "{}{:.2f} {} ({}{:.2f}%)".format( 2196 "+" if data["profit"] > 0 else "", 2197 data["profit"], data["baseCurrencyName"], 2198 "+" if data["percentProfit"] > 0 else "", 2199 data["percentProfit"], 2200 ), 2201 ) 2202 2203 # --- Show currencies section: 2204 if view["stat"]["Currencies"]: 2205 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2206 for item in view["stat"]["Currencies"]: 2207 info.append(_InfoStr(item, isCurr=True)) 2208 2209 else: 2210 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2211 2212 # --- Show shares section: 2213 if view["stat"]["Shares"]: 2214 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2215 2216 for item in view["stat"]["Shares"]: 2217 info.append(_InfoStr(item)) 2218 2219 else: 2220 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2221 2222 # --- Show bonds section: 2223 if view["stat"]["Bonds"]: 2224 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2225 2226 for item in view["stat"]["Bonds"]: 2227 info.append(_InfoStr(item)) 2228 2229 else: 2230 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2231 2232 # --- Show etfs section: 2233 if view["stat"]["Etfs"]: 2234 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2235 2236 for item in view["stat"]["Etfs"]: 2237 info.append(_InfoStr(item)) 2238 2239 else: 2240 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2241 2242 # --- Show futures section: 2243 if view["stat"]["Futures"]: 2244 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2245 2246 for item in view["stat"]["Futures"]: 2247 info.append(_InfoStr(item)) 2248 2249 else: 2250 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2251 2252 if details in ["full", "orders"]: 2253 # --- Show pending limit orders section: 2254 if view["stat"]["orders"]: 2255 info.extend([ 2256 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2257 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2258 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2259 ]) 2260 2261 for item in view["stat"]["orders"]: 2262 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2263 "{} [{}]".format(item["ticker"], item["figi"]), 2264 item["orderID"], 2265 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2266 "{} {} ({}{:.2f}%)".format( 2267 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2268 item["baseCurrencyName"], 2269 "+" if item["percentChanges"] > 0 else "", 2270 float(item["percentChanges"]), 2271 ), 2272 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2273 item["action"], 2274 item["type"], 2275 item["date"], 2276 )) 2277 2278 else: 2279 info.append("\n## Total pending limit-orders: [0]\n") 2280 2281 # --- Show stop orders section: 2282 if view["stat"]["stopOrders"]: 2283 info.extend([ 2284 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2285 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2286 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2287 ]) 2288 2289 for item in view["stat"]["stopOrders"]: 2290 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2291 "{} [{}]".format(item["ticker"], item["figi"]), 2292 item["orderID"], 2293 item["lotsRequested"], 2294 "{} {} ({}{:.2f}%)".format( 2295 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2296 item["baseCurrencyName"], 2297 "+" if item["percentChanges"] > 0 else "", 2298 float(item["percentChanges"]), 2299 ), 2300 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2301 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2302 item["action"], 2303 item["type"], 2304 item["expType"], 2305 item["createDate"], 2306 item["expDate"], 2307 )) 2308 2309 else: 2310 info.append("\n## Total stop-orders: [0]\n") 2311 2312 if details in ["full", "analytics"]: 2313 # -- Show analytics section: 2314 if view["stat"]["portfolioCostRUB"] > 0: 2315 info.extend([ 2316 "\n# Analytics\n\n" 2317 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2318 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2319 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2320 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2321 view["stat"]["totalChangesRUB"], 2322 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2323 view["stat"]["totalChangesPercentRUB"], 2324 ), 2325 "\n## Portfolio distribution by assets\n" 2326 "\n| Type | Uniques | Percent | Current cost |\n", 2327 "|------------------------------------|---------|---------|--------------------|\n", 2328 ]) 2329 2330 for key in view["analytics"]["distrByAssets"].keys(): 2331 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2332 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2333 key, 2334 view["analytics"]["distrByAssets"][key]["uniques"], 2335 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2336 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2337 )) 2338 2339 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2340 2341 info.extend([ 2342 "\n## Portfolio distribution by companies\n" 2343 "\n| Company | Percent | Current cost |\n", 2344 aSepLine, 2345 ]) 2346 2347 for company in view["analytics"]["distrByCompanies"].keys(): 2348 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2349 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2350 "{}{}".format( 2351 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2352 company, 2353 ), 2354 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2355 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2356 )) 2357 2358 info.extend([ 2359 "\n## Portfolio distribution by sectors\n" 2360 "\n| Sector | Percent | Current cost |\n", 2361 aSepLine, 2362 ]) 2363 2364 for sector in view["analytics"]["distrBySectors"].keys(): 2365 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2366 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2367 sector, 2368 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2369 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2370 )) 2371 2372 info.extend([ 2373 "\n## Portfolio distribution by currencies\n" 2374 "\n| Instruments currencies | Percent | Current cost |\n", 2375 aSepLine, 2376 ]) 2377 2378 for curr in view["analytics"]["distrByCurrencies"].keys(): 2379 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2380 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2381 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2382 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2383 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2384 )) 2385 2386 info.extend([ 2387 "\n## Portfolio distribution by countries\n" 2388 "\n| Assets by country | Percent | Current cost |\n", 2389 aSepLine, 2390 ]) 2391 2392 for country in view["analytics"]["distrByCountries"].keys(): 2393 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2394 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2395 country, 2396 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2397 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2398 )) 2399 2400 if details in ["full", "calendar"]: 2401 # -- Show bonds payment calendar section: 2402 if view["stat"]["Bonds"]: 2403 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2404 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2405 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2406 2407 else: 2408 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2409 2410 infoText = "".join(info) 2411 2412 if show and not onlyFiles: 2413 uLogger.info(infoText) 2414 2415 if details == "full" and self.overviewFile: 2416 filename = self.overviewFile 2417 2418 elif details == "digest" and self.overviewDigestFile: 2419 filename = self.overviewDigestFile 2420 2421 elif details == "positions" and self.overviewPositionsFile: 2422 filename = self.overviewPositionsFile 2423 2424 elif details == "orders" and self.overviewOrdersFile: 2425 filename = self.overviewOrdersFile 2426 2427 elif details == "analytics" and self.overviewAnalyticsFile: 2428 filename = self.overviewAnalyticsFile 2429 2430 elif details == "calendar" and self.overviewBondsCalendarFile: 2431 filename = self.overviewBondsCalendarFile 2432 2433 else: 2434 filename = "" 2435 2436 if filename and (show or onlyFiles): 2437 with open(filename, "w", encoding="UTF-8") as fH: 2438 fH.write(infoText) 2439 2440 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2441 2442 if self.useHTMLReports: 2443 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2444 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2445 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2446 2447 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2448 2449 return view 2450 2451 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2452 """ 2453 Returns history operations between two given dates for current `accountId`. 2454 If `reportFile` string is not empty then also save human-readable report. 2455 Shows some statistical data of closed positions. 2456 2457 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2458 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2459 :param show: if `True` then also prints all records to the console. 2460 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2461 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2462 :return: original list of dictionaries with history of deals records from API ("operations" key): 2463 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2464 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2465 """ 2466 if self.accountId is None or not self.accountId: 2467 uLogger.error("Variable `accountId` must be defined for using this method!") 2468 raise Exception("Account ID required") 2469 2470 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2471 2472 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2473 2474 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2475 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2476 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2477 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2478 customStat = {} # custom statistics in additional to responseJSON 2479 2480 # --- output report in human-readable format: 2481 if self.reportFile and (show or onlyFiles): 2482 splitLine1 = "| | | | | |\n" # Summary section 2483 splitLine2 = "| | | | | | | | |\n" # Operations section 2484 nextDay = "" 2485 2486 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2487 2488 if len(ops) > 0: 2489 customStat = { 2490 "opsCount": 0, # total operations count 2491 "buyCount": 0, # buy operations 2492 "sellCount": 0, # sell operations 2493 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2494 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2495 "payIn": {"rub": 0.}, # Deposit brokerage account 2496 "payOut": {"rub": 0.}, # Withdrawals 2497 "divs": {"rub": 0.}, # Dividends income 2498 "coupons": {"rub": 0.}, # Coupon's income 2499 "brokerCom": {"rub": 0.}, # Service commissions 2500 "serviceCom": {"rub": 0.}, # Service commissions 2501 "marginCom": {"rub": 0.}, # Margin commissions 2502 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2503 } 2504 2505 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2506 for item in ops: 2507 if item["state"] == "OPERATION_STATE_EXECUTED": 2508 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2509 2510 # count buy operations: 2511 if "_BUY" in item["operationType"]: 2512 customStat["buyCount"] += 1 2513 2514 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2515 customStat["buyTotal"][item["payment"]["currency"]] += payment 2516 2517 else: 2518 customStat["buyTotal"][item["payment"]["currency"]] = payment 2519 2520 # count sell operations: 2521 elif "_SELL" in item["operationType"]: 2522 customStat["sellCount"] += 1 2523 2524 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2525 customStat["sellTotal"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["sellTotal"][item["payment"]["currency"]] = payment 2529 2530 # count incoming operations: 2531 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2532 if item["payment"]["currency"] in customStat["payIn"].keys(): 2533 customStat["payIn"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["payIn"][item["payment"]["currency"]] = payment 2537 2538 # count withdrawals operations: 2539 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2540 if item["payment"]["currency"] in customStat["payOut"].keys(): 2541 customStat["payOut"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["payOut"][item["payment"]["currency"]] = payment 2545 2546 # count dividends income: 2547 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2548 if item["payment"]["currency"] in customStat["divs"].keys(): 2549 customStat["divs"][item["payment"]["currency"]] += payment 2550 2551 else: 2552 customStat["divs"][item["payment"]["currency"]] = payment 2553 2554 # count coupon's income: 2555 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2556 if item["payment"]["currency"] in customStat["coupons"].keys(): 2557 customStat["coupons"][item["payment"]["currency"]] += payment 2558 2559 else: 2560 customStat["coupons"][item["payment"]["currency"]] = payment 2561 2562 # count broker commissions: 2563 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2564 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2565 customStat["brokerCom"][item["payment"]["currency"]] += payment 2566 2567 else: 2568 customStat["brokerCom"][item["payment"]["currency"]] = payment 2569 2570 # count service commissions: 2571 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2572 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2573 customStat["serviceCom"][item["payment"]["currency"]] += payment 2574 2575 else: 2576 customStat["serviceCom"][item["payment"]["currency"]] = payment 2577 2578 # count margin commissions: 2579 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2580 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2581 customStat["marginCom"][item["payment"]["currency"]] += payment 2582 2583 else: 2584 customStat["marginCom"][item["payment"]["currency"]] = payment 2585 2586 # count withholding taxes: 2587 elif "_TAX" in item["operationType"]: 2588 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2589 customStat["allTaxes"][item["payment"]["currency"]] += payment 2590 2591 else: 2592 customStat["allTaxes"][item["payment"]["currency"]] = payment 2593 2594 else: 2595 continue 2596 2597 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2598 2599 # --- view "Actions" lines: 2600 info.extend([ 2601 "| Report sections | | | | |\n", 2602 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2603 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2604 "| | Buy: {:<22} | {:<28} | | |\n".format( 2605 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2606 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2607 ), 2608 "| | Sell: {:<21} | {:<28} | | |\n".format( 2609 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2610 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2611 ), 2612 ]) 2613 2614 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2615 for key in opsKeys: 2616 if key == "rub": 2617 continue 2618 2619 info.extend([ 2620 "| | | {:<28} | | |\n".format( 2621 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2622 ), 2623 "| | | {:<28} | | |\n".format( 2624 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2625 ), 2626 ]) 2627 2628 info.append(splitLine1) 2629 2630 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2631 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2632 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2633 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2634 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2635 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2636 ) 2637 2638 # --- view "Payments" lines: 2639 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2640 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2641 2642 for key in paymentsKeys: 2643 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2644 2645 info.append(splitLine1) 2646 2647 # --- view "Commissions and taxes" lines: 2648 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2649 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2650 2651 for key in comKeys: 2652 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2653 2654 info.extend([ 2655 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2656 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2657 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2658 ]) 2659 2660 else: 2661 info.append("Broker returned no operations during this period\n") 2662 2663 # --- view "Operations" section: 2664 for item in ops: 2665 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2666 continue 2667 2668 else: 2669 self._figi = item["figi"] 2670 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2671 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2672 2673 # group of deals during one day: 2674 if nextDay and item["date"].split("T")[0] != nextDay: 2675 info.append(splitLine2) 2676 nextDay = "" 2677 2678 else: 2679 nextDay = item["date"].split("T")[0] # saving current day for splitting 2680 2681 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2682 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2683 self._figi if self._figi else "—", 2684 instrument["ticker"] if instrument else "—", 2685 instrument["type"] if instrument else "—", 2686 item["quantity"] if int(item["quantity"]) > 0 else "—", 2687 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2688 TKS_OPERATION_STATES[item["state"]], 2689 TKS_OPERATION_TYPES[item["operationType"]], 2690 )) 2691 2692 infoText = "".join(info) 2693 2694 if show and not onlyFiles: 2695 if self.moreDebug: 2696 uLogger.debug("Records about history of a client's operations successfully received") 2697 2698 uLogger.info(infoText) 2699 2700 if self.reportFile and (show or onlyFiles): 2701 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2702 fH.write(infoText) 2703 2704 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2705 2706 if self.useHTMLReports: 2707 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2708 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2709 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2710 2711 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2712 2713 return ops, customStat 2714 2715 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2716 """ 2717 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2718 2719 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2720 Warning! Broker server used ISO UTC time by default. 2721 2722 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2723 Also, `historyFile` used to update history with `onlyMissing` parameter. 2724 2725 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2726 2727 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2728 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2729 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2730 `"hour"`, `"day"`. Default: `"hour"`. 2731 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2732 False by default. Warning! History appends only from last candle to current time 2733 with always update last candle! 2734 :param csvSep: separator if csv-file is used, `,` by default. 2735 :param show: if `True` then also prints Pandas DataFrame to the console. 2736 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2737 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2738 `["date", "time", "open", "high", "low", "close", "volume"]`. 2739 """ 2740 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2741 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2742 history = None # empty pandas object for history 2743 2744 if interval not in TKS_CANDLE_INTERVALS.keys(): 2745 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2746 raise Exception("Incorrect value") 2747 2748 if not (self._ticker or self._figi): 2749 uLogger.error("Ticker or FIGI must be defined!") 2750 raise Exception("Ticker or FIGI required") 2751 2752 if self._ticker and not self._figi: 2753 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2754 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2755 2756 if self._figi and not self._ticker: 2757 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2758 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2759 2760 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2761 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2762 if interval.lower() != "day": 2763 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2764 2765 delta = dtEnd - dtStart # current UTC time minus last time in file 2766 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2767 2768 # calculate history length in candles: 2769 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2770 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2771 length += 1 # to avoid fraction time 2772 2773 # calculate data blocks count: 2774 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2775 2776 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2777 if self.moreDebug: 2778 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2779 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2780 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2781 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2782 2783 tempOld = None # pandas object for old history, if --only-missing key present 2784 lastTime = None # datetime object of last old candle in file 2785 2786 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2787 if self.moreDebug: 2788 uLogger.debug("--only-missing key present, add only last missing candles...") 2789 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2790 2791 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2792 2793 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2794 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2795 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2796 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2797 2798 # get last datetime object from last string in file or minus 1 delta if file is empty: 2799 if len(tempOld) > 0: 2800 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2801 2802 else: 2803 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2804 2805 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2806 2807 responseJSONs = [] # raw history blocks of data 2808 2809 blockEnd = dtEnd 2810 for item in range(blocks): 2811 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2812 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2813 2814 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2815 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2816 )) 2817 2818 if blockStart == blockEnd: 2819 uLogger.debug("Skipped this zero-length block...") 2820 2821 else: 2822 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2823 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2824 self.body = str({ 2825 "figi": self._figi, 2826 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2827 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2828 "interval": TKS_CANDLE_INTERVALS[interval][0] 2829 }) 2830 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2831 2832 if "code" in responseJSON.keys(): 2833 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2834 2835 else: 2836 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2837 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2838 2839 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2840 2841 blockEnd = blockStart 2842 2843 printCount = len(responseJSONs) # candles to show in console 2844 if responseJSONs: 2845 tempHistory = pd.DataFrame( 2846 data={ 2847 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2848 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2849 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2850 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2851 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2852 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2853 "volume": [int(item["volume"]) for item in responseJSONs], 2854 }, 2855 index=range(len(responseJSONs)), 2856 columns=["date", "time", "open", "high", "low", "close", "volume"], 2857 ) 2858 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2859 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2860 2861 # append only newest candles to old history if --only-missing key present: 2862 if onlyMissing and tempOld is not None and lastTime is not None: 2863 index = 0 # find start index in tempHistory data: 2864 2865 for i, item in tempHistory.iterrows(): 2866 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2867 2868 if curTime == lastTime: 2869 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2870 index = i 2871 printCount = i + 1 2872 break 2873 2874 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2875 2876 else: 2877 history = tempHistory # if no `--only-missing` key then load full data from server 2878 2879 if self.moreDebug: 2880 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2881 2882 if history is not None and not history.empty: 2883 if show and not onlyFiles: 2884 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2885 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2886 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2887 )) 2888 2889 else: 2890 uLogger.warning("Received an empty candles history!") 2891 2892 if self.historyFile is not None: 2893 if history is not None and not history.empty: 2894 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2895 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2896 2897 else: 2898 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2899 2900 else: 2901 if self.moreDebug: 2902 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2903 2904 return history 2905 2906 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2907 """ 2908 Load candles history from csv-file and return Pandas DataFrame object. 2909 2910 See also: `History()` and `ShowHistoryChart()` methods. 2911 2912 :param filePath: path to csv-file to open. 2913 """ 2914 loadedHistory = None # init candles data object 2915 2916 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2917 2918 if os.path.exists(filePath): 2919 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2920 2921 tfStr = self.priceModel.FormattedDelta( 2922 self.priceModel.timeframe, 2923 "{days} days {hours}h {minutes}m {seconds}s", 2924 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2925 self.priceModel.timeframe, 2926 "{hours}h {minutes}m {seconds}s", 2927 ) 2928 2929 if loadedHistory is not None and not loadedHistory.empty: 2930 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2931 len(loadedHistory), 2932 tfStr, 2933 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2934 ) 2935 2936 else: 2937 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2938 2939 else: 2940 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2941 2942 return loadedHistory 2943 2944 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2945 """ 2946 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2947 2948 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2949 Default: `index.html` (both for interact and non-interact candlesticks chart). 2950 2951 See also: `History()` and `LoadHistory()` methods. 2952 2953 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2954 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2955 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2956 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2957 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2958 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2959 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2960 """ 2961 if isinstance(candles, str): 2962 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2963 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2964 2965 elif isinstance(candles, pd.DataFrame): 2966 self.priceModel.prices = candles # set candles chain from variable 2967 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2968 2969 if "datetime" not in candles.columns: 2970 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2971 2972 else: 2973 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2974 raise Exception("Incorrect value") 2975 2976 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2977 2978 if interact: 2979 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2980 2981 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2982 2983 else: 2984 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2985 2986 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2987 2988 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2989 2990 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2991 """ 2992 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2993 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2994 2995 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2996 2997 :param operation: string "Buy" or "Sell". 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3000 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3001 :param expDate: string "Undefined" by default or local date in future, 3002 it is a string with format `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 if self.accountId is None or not self.accountId: 3006 uLogger.error("Variable `accountId` must be defined for using this method!") 3007 raise Exception("Account ID required") 3008 3009 if operation is None or not operation or operation not in ("Buy", "Sell"): 3010 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3011 raise Exception("Incorrect value") 3012 3013 if lots is None or lots < 1: 3014 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3015 lots = 1 3016 3017 if tp is None or tp < 0: 3018 tp = 0 3019 3020 if sl is None or sl < 0: 3021 sl = 0 3022 3023 if expDate is None or not expDate: 3024 expDate = "Undefined" 3025 3026 if not (self._ticker or self._figi): 3027 uLogger.error("Ticker or FIGI must be defined!") 3028 raise Exception("Ticker or FIGI required") 3029 3030 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3031 self._ticker = instrument["ticker"] 3032 self._figi = instrument["figi"] 3033 3034 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3035 3036 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3037 self.body = str({ 3038 "figi": self._figi, 3039 "quantity": str(lots), 3040 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3041 "accountId": str(self.accountId), 3042 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3043 }) 3044 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3045 3046 if "orderId" in response.keys(): 3047 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3048 operation, response["orderId"], 3049 self._ticker, self._figi, lots, 3050 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3051 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3052 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3053 )) 3054 3055 if tp > 0: 3056 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3057 3058 if sl > 0: 3059 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3060 3061 else: 3062 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3063 3064 return response 3065 3066 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3067 """ 3068 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3069 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3070 3071 See also: `Order()` and `Trade()` docstrings. 3072 3073 :param lots: volume, integer count of lots >= 1. 3074 :param tp: float > 0, take profit price of stop-order. 3075 :param sl: float > 0, stop loss price of stop-order. 3076 :param expDate: it's a local date in future. 3077 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3078 :return: JSON with response from broker server. 3079 """ 3080 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3081 3082 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3083 """ 3084 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3085 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3086 3087 See also: `Order()` and `Trade()` docstrings. 3088 3089 :param lots: volume, integer count of lots >= 1. 3090 :param tp: float > 0, take profit price of stop-order. 3091 :param sl: float > 0, stop loss price of stop-order. 3092 :param expDate: it's a local date in the future. 3093 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3094 :return: JSON with response from broker server. 3095 """ 3096 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3097 3098 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3099 """ 3100 Close position of given instruments. 3101 3102 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3103 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3104 This avoids unnecessary downloading data from the server. 3105 """ 3106 if instruments is None or not instruments: 3107 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3108 raise Exception("Ticker or FIGI required") 3109 3110 if isinstance(instruments, str): 3111 instruments = [instruments] 3112 3113 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3114 if uniqueInstruments: 3115 if portfolio is None or not portfolio: 3116 portfolio = self.Overview(show=False) 3117 3118 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3119 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3120 3121 for self._figi in uniqueInstruments: 3122 if self._figi not in allOpened: 3123 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3124 continue 3125 3126 # search open trade info about instrument by ticker: 3127 instrument = {} 3128 for iType in TKS_INSTRUMENTS: 3129 if instrument: 3130 break 3131 3132 for item in portfolio["stat"][iType]: 3133 if item["figi"] == self._figi: 3134 instrument = item 3135 break 3136 3137 if instrument: 3138 self._ticker = instrument["ticker"] 3139 self._figi = instrument["figi"] 3140 3141 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3142 self._ticker, 3143 self._figi, 3144 int(instrument["volume"]), 3145 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3146 )) 3147 3148 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3149 3150 if tradeLots > 0: 3151 if instrument["blocked"] > 0: 3152 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3153 instrument["blocked"], 3154 self._ticker, 3155 tradeLots, 3156 )) 3157 3158 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3159 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3160 3161 else: 3162 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3163 3164 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3165 """ 3166 Close all positions of given instruments with defined type. 3167 3168 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3169 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3170 This avoids unnecessary downloading data from the server. 3171 """ 3172 if iType not in TKS_INSTRUMENTS: 3173 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3174 3175 else: 3176 if portfolio is None or not portfolio: 3177 portfolio = self.Overview(show=False) 3178 3179 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3180 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3181 3182 if tickers and portfolio: 3183 self.CloseTrades(tickers, portfolio) 3184 3185 else: 3186 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3187 3188 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3189 """ 3190 Universal method to create market or limit orders with all available parameters for current `accountId`. 3191 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3192 3193 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3194 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3195 3196 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3197 then broker immediately open market order as you can do simple --buy or --sell operations! 3198 3199 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3200 When current price will go up or down to target price value then broker opens a limit order. 3201 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3202 3203 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3204 3205 :param operation: string "Buy" or "Sell". 3206 :param orderType: string "Limit" or "Stop". 3207 :param lots: volume, integer count of lots >= 1. 3208 :param targetPrice: target price > 0. This is open trade price for limit order. 3209 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3210 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3211 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3212 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3213 Stop loss order always executed by market price. 3214 :param expDate: string "Undefined" by default or local date in future. 3215 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3216 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3217 A limit order has no expiration date, it lasts until the end of the trading day. 3218 :return: JSON with response from broker server. 3219 """ 3220 if self.accountId is None or not self.accountId: 3221 uLogger.error("Variable `accountId` must be defined for using this method!") 3222 raise Exception("Account ID required") 3223 3224 if operation is None or not operation or operation not in ("Buy", "Sell"): 3225 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3226 raise Exception("Incorrect value") 3227 3228 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3229 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3230 raise Exception("Incorrect value") 3231 3232 if lots is None or lots < 1: 3233 uLogger.error("You must define trade volume > 0: integer count of lots!") 3234 raise Exception("Incorrect value") 3235 3236 if targetPrice is None or targetPrice <= 0: 3237 uLogger.error("Target price for limit-order must be greater than 0!") 3238 raise Exception("Incorrect value") 3239 3240 if limitPrice is None or limitPrice <= 0: 3241 limitPrice = targetPrice 3242 3243 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3244 stopType = "Limit" 3245 3246 if expDate is None or not expDate: 3247 expDate = "Undefined" 3248 3249 if not (self._ticker or self._figi): 3250 uLogger.error("Tocker or FIGI must be defined!") 3251 raise Exception("Ticker or FIGI required") 3252 3253 response = {} 3254 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3255 self._ticker = instrument["ticker"] 3256 self._figi = instrument["figi"] 3257 3258 if orderType == "Limit": 3259 uLogger.debug( 3260 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3261 self._ticker, self._figi, 3262 operation, lots, targetPrice, instrument["currency"], 3263 )) 3264 3265 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3266 self.body = str({ 3267 "figi": self._figi, 3268 "quantity": str(lots), 3269 "price": FloatToNano(targetPrice), 3270 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3271 "accountId": str(self.accountId), 3272 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3273 }) 3274 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3275 3276 if "orderId" in response.keys(): 3277 uLogger.info( 3278 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3279 response["orderId"], self._ticker, self._figi, operation, lots, 3280 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3281 )) 3282 3283 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3284 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3285 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3286 targetPrice, instrument["currency"], 3287 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3288 )) 3289 3290 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3291 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3292 targetPrice, instrument["currency"], 3293 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3294 )) 3295 3296 else: 3297 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3298 3299 if orderType == "Stop": 3300 uLogger.debug( 3301 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3302 self._ticker, self._figi, 3303 operation, lots, 3304 targetPrice, instrument["currency"], 3305 limitPrice, instrument["currency"], 3306 stopType, expDate, 3307 )) 3308 3309 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3310 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3311 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3312 3313 body = { 3314 "figi": self._figi, 3315 "quantity": str(lots), 3316 "price": FloatToNano(limitPrice), 3317 "stopPrice": FloatToNano(targetPrice), 3318 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3319 "accountId": str(self.accountId), 3320 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3321 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3322 } 3323 3324 if expDateUTC: 3325 body["expireDate"] = expDateUTC 3326 3327 self.body = str(body) 3328 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3329 3330 if "stopOrderId" in response.keys(): 3331 uLogger.info( 3332 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3333 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3334 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3335 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3336 TKS_STOP_ORDER_TYPES[stopOrderType], 3337 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3338 )) 3339 3340 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3341 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3342 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3343 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3344 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3345 )) 3346 3347 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3348 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3349 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3350 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3351 )) 3352 3353 else: 3354 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3355 3356 return response 3357 3358 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3359 """ 3360 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3361 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3362 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3363 See also: `Order()` docstring. 3364 3365 :param lots: volume, integer count of lots >= 1. 3366 :param targetPrice: target price > 0. This is open trade price for limit order. 3367 :return: JSON with response from broker server. 3368 """ 3369 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3370 3371 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3372 """ 3373 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3374 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3375 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3376 target price value then broker opens a limit order. See also: `Order()` docstring. 3377 3378 :param lots: volume, integer count of lots >= 1. 3379 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3380 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3381 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3382 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3383 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3384 :param expDate: string "Undefined" by default or local date in future. 3385 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3386 This date is converting to UTC format for server. 3387 :return: JSON with response from broker server. 3388 """ 3389 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3390 3391 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3392 """ 3393 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3394 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3395 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3396 See also: `Order()` docstring. 3397 3398 :param lots: volume, integer count of lots >= 1. 3399 :param targetPrice: target price > 0. This is open trade price for limit order. 3400 :return: JSON with response from broker server. 3401 """ 3402 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3403 3404 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3405 """ 3406 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3407 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3408 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3409 target price value then broker opens a limit order. See also: `Order()` docstring. 3410 3411 :param lots: volume, integer count of lots >= 1. 3412 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3413 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3414 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3415 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3416 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3417 :param expDate: string "Undefined" by default or local date in future. 3418 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3419 This date is converting to UTC format for server. 3420 :return: JSON with response from broker server. 3421 """ 3422 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3423 3424 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3425 """ 3426 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3427 3428 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3429 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3430 This avoids unnecessary downloading data from the server. 3431 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3432 """ 3433 if self.accountId is None or not self.accountId: 3434 uLogger.error("Variable `accountId` must be defined for using this method!") 3435 raise Exception("Account ID required") 3436 3437 if orderIDs: 3438 if allOrdersIDs is None: 3439 rawOrders = self.RequestPendingOrders() 3440 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3441 3442 if allStopOrdersIDs is None: 3443 rawStopOrders = self.RequestStopOrders() 3444 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3445 3446 for orderID in orderIDs: 3447 idInPendingOrders = orderID in allOrdersIDs 3448 idInStopOrders = orderID in allStopOrdersIDs 3449 3450 if not (idInPendingOrders or idInStopOrders): 3451 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3452 continue 3453 3454 else: 3455 if idInPendingOrders: 3456 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3457 3458 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3459 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3460 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3461 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3462 3463 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3464 if self.moreDebug: 3465 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3466 3467 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3468 3469 else: 3470 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3471 3472 elif idInStopOrders: 3473 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3474 3475 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3476 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3477 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3478 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3479 3480 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3481 if self.moreDebug: 3482 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3483 3484 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3485 3486 else: 3487 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3488 3489 else: 3490 continue 3491 3492 def CloseAllOrders(self) -> None: 3493 """ 3494 Gets a list of open pending and stop orders and cancel it all. 3495 """ 3496 rawOrders = self.RequestPendingOrders() 3497 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3498 lenOrders = len(allOrdersIDs) 3499 3500 rawStopOrders = self.RequestStopOrders() 3501 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3502 lenSOrders = len(allStopOrdersIDs) 3503 3504 if lenOrders > 0 or lenSOrders > 0: 3505 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3506 3507 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3508 3509 else: 3510 uLogger.info("Orders not found, nothing to cancel.") 3511 3512 def CloseAll(self, *args) -> None: 3513 """ 3514 Close all available (not blocked) opened trades and orders. 3515 3516 Also, you can select one or more keywords case-insensitive: 3517 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3518 3519 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3520 """ 3521 overview = self.Overview(show=False) # get all open trades info 3522 3523 if len(args) == 0: 3524 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3525 self.CloseAllOrders() # close all pending and stop orders 3526 3527 for iType in TKS_INSTRUMENTS: 3528 if iType != "Currencies": 3529 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3530 3531 else: 3532 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3533 lowerArgs = [x.lower() for x in args] 3534 3535 if "orders" in lowerArgs: 3536 self.CloseAllOrders() # close all pending and stop orders 3537 3538 for iType in TKS_INSTRUMENTS: 3539 if iType.lower() in lowerArgs and iType != "Currencies": 3540 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3541 3542 def CloseAllByTicker(self, instrument: str) -> None: 3543 """ 3544 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3545 3546 This method searches opened trade and orders of instrument throw all portfolio and then use 3547 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3548 3549 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3550 3551 :param instrument: string with ticker. 3552 """ 3553 if instrument is None or not instrument: 3554 uLogger.error("Ticker name must be defined for using this method!") 3555 raise Exception("Ticker required") 3556 3557 overview = self.Overview(show=False) # get user portfolio with all open trades info 3558 3559 self._ticker = instrument # try to set instrument as ticker 3560 self._figi = "" 3561 3562 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3563 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3564 3565 if limitAll and self.IsInLimitOrders(portfolio=overview): 3566 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3567 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3568 3569 if stopAll and self.IsInStopOrders(portfolio=overview): 3570 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3571 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3572 3573 if self.IsInPortfolio(portfolio=overview): 3574 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3575 self.CloseTrades(instruments=[instrument], portfolio=overview) 3576 3577 def CloseAllByFIGI(self, instrument: str) -> None: 3578 """ 3579 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3580 3581 This method searches opened trade and orders of instrument throw all portfolio and then use 3582 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3583 3584 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3585 3586 :param instrument: string with FIGI id. 3587 """ 3588 if instrument is None or not instrument: 3589 uLogger.error("FIGI id must be defined for using this method!") 3590 raise Exception("FIGI required") 3591 3592 overview = self.Overview(show=False) # get user portfolio with all open trades info 3593 3594 self._ticker = "" 3595 self._figi = instrument # try to set instrument as FIGI id 3596 3597 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3598 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3599 3600 if limitAll and self.IsInLimitOrders(portfolio=overview): 3601 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3602 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3603 3604 if stopAll and self.IsInStopOrders(portfolio=overview): 3605 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3606 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3607 3608 if self.IsInPortfolio(portfolio=overview): 3609 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3610 self.CloseTrades(instruments=[instrument], portfolio=overview) 3611 3612 @staticmethod 3613 def ParseOrderParameters(operation, **inputParameters): 3614 """ 3615 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3616 3617 :param operation: string "Buy" or "Sell". 3618 :param inputParameters: this is dict of strings that looks like this 3619 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3620 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3621 "prices" key: one or more prices to open limit-orders 3622 Counts of values in lots and prices lists must be equals! 3623 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3624 """ 3625 # TODO: update order grid work with api v2 3626 pass 3627 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3628 # 3629 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3630 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3631 # raise Exception("Incorrect value") 3632 # 3633 # if "l" in inputParameters.keys(): 3634 # inputParameters["lots"] = inputParameters.pop("l") 3635 # 3636 # if "p" in inputParameters.keys(): 3637 # inputParameters["prices"] = inputParameters.pop("p") 3638 # 3639 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3640 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3641 # raise Exception("Incorrect value") 3642 # 3643 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3644 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3645 # 3646 # if len(lots) != len(prices): 3647 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3648 # raise Exception("Incorrect value") 3649 # 3650 # uLogger.debug("Extracted parameters for orders:") 3651 # uLogger.debug("lots = {}".format(lots)) 3652 # uLogger.debug("prices = {}".format(prices)) 3653 # 3654 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3655 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3656 # uLogger.debug("Order parameters: {}".format(result)) 3657 # 3658 # return result 3659 3660 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3661 """ 3662 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3663 3664 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3665 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3666 """ 3667 result = False 3668 msg = "Instrument not defined!" 3669 3670 if portfolio is None or not portfolio: 3671 portfolio = self.Overview(show=False) 3672 3673 if self._ticker: 3674 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3675 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3676 3677 for iType in TKS_INSTRUMENTS: 3678 for instrument in portfolio["stat"][iType]: 3679 if instrument["ticker"] == self._ticker: 3680 result = True 3681 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3682 break 3683 3684 elif self._figi: 3685 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3686 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3687 3688 for iType in TKS_INSTRUMENTS: 3689 for instrument in portfolio["stat"][iType]: 3690 if instrument["figi"] == self._figi: 3691 result = True 3692 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3693 break 3694 3695 else: 3696 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3697 3698 uLogger.debug(msg) 3699 3700 return result 3701 3702 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3703 """ 3704 Returns instrument from the user's portfolio if it presents there. 3705 Instrument must be defined by `ticker` (highly priority) or `figi`. 3706 3707 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3708 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3709 """ 3710 result = None 3711 msg = "Instrument not defined!" 3712 3713 if portfolio is None or not portfolio: 3714 portfolio = self.Overview(show=False) 3715 3716 if self._ticker: 3717 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3718 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3719 3720 for iType in TKS_INSTRUMENTS: 3721 for instrument in portfolio["stat"][iType]: 3722 if instrument["ticker"] == self._ticker: 3723 result = instrument 3724 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3725 break 3726 3727 elif self._figi: 3728 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3729 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3730 3731 for iType in TKS_INSTRUMENTS: 3732 for instrument in portfolio["stat"][iType]: 3733 if instrument["figi"] == self._figi: 3734 result = instrument 3735 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3736 break 3737 3738 else: 3739 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3740 3741 uLogger.debug(msg) 3742 3743 return result 3744 3745 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3746 """ 3747 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3748 3749 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3750 3751 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3752 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3753 """ 3754 result = False 3755 msg = "Instrument not defined!" 3756 3757 if portfolio is None or not portfolio: 3758 portfolio = self.Overview(show=False) 3759 3760 if self._ticker: 3761 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3762 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3763 3764 for instrument in portfolio["stat"]["orders"]: 3765 if instrument["ticker"] == self._ticker: 3766 result = True 3767 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3768 break 3769 3770 elif self._figi: 3771 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3772 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3773 3774 for instrument in portfolio["stat"]["orders"]: 3775 if instrument["figi"] == self._figi: 3776 result = True 3777 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3778 break 3779 3780 else: 3781 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3782 3783 uLogger.debug(msg) 3784 3785 return result 3786 3787 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3788 """ 3789 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3790 Instrument must be defined by `ticker` (highly priority) or `figi`. 3791 3792 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3793 3794 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3795 :return: list with `orderID`s of limit orders. 3796 """ 3797 result = [] 3798 msg = "Instrument not defined!" 3799 3800 if portfolio is None or not portfolio: 3801 portfolio = self.Overview(show=False) 3802 3803 if self._ticker: 3804 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3805 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3806 3807 for instrument in portfolio["stat"]["orders"]: 3808 if instrument["ticker"] == self._ticker: 3809 result.append(instrument["orderID"]) 3810 3811 if result: 3812 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3813 3814 elif self._figi: 3815 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3816 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3817 3818 for instrument in portfolio["stat"]["orders"]: 3819 if instrument["figi"] == self._figi: 3820 result.append(instrument["orderID"]) 3821 3822 if result: 3823 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3824 3825 else: 3826 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3827 3828 uLogger.debug(msg) 3829 3830 return result 3831 3832 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3833 """ 3834 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3835 3836 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3837 3838 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3839 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3840 """ 3841 result = False 3842 msg = "Instrument not defined!" 3843 3844 if portfolio is None or not portfolio: 3845 portfolio = self.Overview(show=False) 3846 3847 if self._ticker: 3848 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3849 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3850 3851 for instrument in portfolio["stat"]["stopOrders"]: 3852 if instrument["ticker"] == self._ticker: 3853 result = True 3854 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3855 break 3856 3857 elif self._figi: 3858 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3859 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3860 3861 for instrument in portfolio["stat"]["stopOrders"]: 3862 if instrument["figi"] == self._figi: 3863 result = True 3864 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3865 break 3866 3867 else: 3868 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3869 3870 uLogger.debug(msg) 3871 3872 return result 3873 3874 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3875 """ 3876 Returns list with all `orderID`s of opened stop orders for the instrument. 3877 Instrument must be defined by `ticker` (highly priority) or `figi`. 3878 3879 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3880 3881 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3882 :return: list with `orderID`s of stop orders. 3883 """ 3884 result = [] 3885 msg = "Instrument not defined!" 3886 3887 if portfolio is None or not portfolio: 3888 portfolio = self.Overview(show=False) 3889 3890 if self._ticker: 3891 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3892 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3893 3894 for instrument in portfolio["stat"]["stopOrders"]: 3895 if instrument["ticker"] == self._ticker: 3896 result.append(instrument["orderID"]) 3897 3898 if result: 3899 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3900 3901 elif self._figi: 3902 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3903 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3904 3905 for instrument in portfolio["stat"]["stopOrders"]: 3906 if instrument["figi"] == self._figi: 3907 result.append(instrument["orderID"]) 3908 3909 if result: 3910 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3911 3912 else: 3913 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3914 3915 uLogger.debug(msg) 3916 3917 return result 3918 3919 def RequestLimits(self) -> dict: 3920 """ 3921 Method for obtaining the available funds for withdrawal for current `accountId`. 3922 3923 See also: 3924 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3925 - `OverviewLimits()` method 3926 3927 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3928 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3929 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3930 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3931 """ 3932 if self.accountId is None or not self.accountId: 3933 uLogger.error("Variable `accountId` must be defined for using this method!") 3934 raise Exception("Account ID required") 3935 3936 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3937 3938 self.body = str({"accountId": self.accountId}) 3939 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3940 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3941 3942 if self.moreDebug: 3943 uLogger.debug("Records about available funds for withdrawal successfully received") 3944 3945 return rawLimits 3946 3947 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3948 """ 3949 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3950 3951 See also: `RequestLimits()`. 3952 3953 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3954 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3955 :return: dict with raw parsed data from server and some calculated statistics about it. 3956 """ 3957 if self.accountId is None or not self.accountId: 3958 uLogger.error("Variable `accountId` must be defined for using this method!") 3959 raise Exception("Account ID required") 3960 3961 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3962 3963 view = { 3964 "rawLimits": rawLimits, 3965 "limits": { # parsed data for every currency: 3966 "money": { # this is an array of portfolio currency positions 3967 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3968 }, 3969 "blocked": { # this is an array of blocked currency 3970 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3971 }, 3972 "blockedGuarantee": { # this is locked money under collateral for futures 3973 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3974 }, 3975 }, 3976 } 3977 3978 # --- Prepare text table with limits in human-readable format: 3979 if show or onlyFiles: 3980 info = [ 3981 "# Withdrawal limits\n\n", 3982 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3983 "* **Account ID:** [{}]\n".format(self.accountId), 3984 ] 3985 3986 if view["limits"]["money"]: 3987 info.extend([ 3988 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3989 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3990 ]) 3991 3992 else: 3993 info.append("\nNo withdrawal limits\n") 3994 3995 for curr in view["limits"]["money"].keys(): 3996 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3997 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3998 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3999 4000 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4001 "[{}]".format(curr), 4002 "{:.2f}".format(view["limits"]["money"][curr]), 4003 "{:.2f}".format(availableMoney), 4004 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4005 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4006 ) 4007 4008 if curr == "rub": 4009 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4010 4011 else: 4012 info.append(infoStr) 4013 4014 infoText = "".join(info) 4015 4016 if show and not onlyFiles: 4017 uLogger.info(infoText) 4018 4019 if self.withdrawalLimitsFile and (show or onlyFiles): 4020 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4021 fH.write(infoText) 4022 4023 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4024 4025 if self.useHTMLReports: 4026 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4027 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4028 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4029 4030 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4031 4032 return view 4033 4034 def RequestAccounts(self) -> dict: 4035 """ 4036 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4037 4038 See also: 4039 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4040 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4041 - `OverviewUserInfo()` method 4042 4043 :return: dict with raw data from server that contains accounts info. Example of dict: 4044 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4045 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4046 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4047 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4048 """ 4049 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4050 4051 self.body = str({}) 4052 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4053 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4054 4055 if self.moreDebug: 4056 uLogger.debug("Records about available accounts successfully received") 4057 4058 return rawAccounts 4059 4060 def RequestUserInfo(self) -> dict: 4061 """ 4062 Method for requesting common user's information. 4063 4064 See also: 4065 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4066 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4067 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4068 - `OverviewUserInfo()` method 4069 4070 :return: dict with raw data from server that contains user's information. Example of dict: 4071 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4072 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4073 """ 4074 uLogger.debug("Requesting common user's information. Wait, please...") 4075 4076 self.body = str({}) 4077 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4078 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4079 4080 if self.moreDebug: 4081 uLogger.debug("Records about current user successfully received") 4082 4083 return rawUserInfo 4084 4085 def RequestMarginStatus(self, accountId: str = None) -> dict: 4086 """ 4087 Method for requesting margin calculation for defined account ID. 4088 4089 See also: 4090 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4091 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4092 - `OverviewUserInfo()` method 4093 4094 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4095 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4096 Example of responses: 4097 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4098 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4099 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4100 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4101 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4102 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4103 """ 4104 if accountId is None or not accountId: 4105 if self.accountId is None or not self.accountId: 4106 uLogger.error("Variable `accountId` must be defined for using this method!") 4107 raise Exception("Account ID required") 4108 4109 else: 4110 accountId = self.accountId # use `self.accountId` (main ID) by default 4111 4112 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4113 4114 self.body = str({"accountId": accountId}) 4115 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4116 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4117 4118 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4119 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4120 rawMargin = {} 4121 4122 else: 4123 if self.moreDebug: 4124 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4125 4126 return rawMargin 4127 4128 def RequestTariffLimits(self) -> dict: 4129 """ 4130 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4131 4132 See also: 4133 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4134 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4135 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4136 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4137 - `OverviewUserInfo()` method 4138 4139 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4140 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4141 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4142 """ 4143 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4144 4145 self.body = str({}) 4146 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4147 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4148 4149 if self.moreDebug: 4150 uLogger.debug("Records with limits of current tariff successfully received") 4151 4152 return rawTariffLimits 4153 4154 def RequestBondCoupons(self, iJSON: dict) -> dict: 4155 """ 4156 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4157 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4158 All dates are in UTC timezone. 4159 4160 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4161 Documentation: 4162 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4163 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4164 4165 See also: `ExtendBondsData()`. 4166 4167 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4168 If raw iJSON is not data of bond then server returns an error [400] with message: 4169 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4170 :return: dictionary with bond payment calendar. Response example 4171 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4172 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4173 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4174 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4175 """ 4176 if iJSON["figi"] is None or not iJSON["figi"]: 4177 uLogger.error("FIGI must be defined for using this method!") 4178 raise Exception("FIGI required") 4179 4180 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4181 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4182 4183 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4184 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4185 self._figi, 4186 startDate, 4187 endDate, 4188 )) 4189 4190 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4191 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4192 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4193 4194 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4195 uLogger.warning("Instrument type is not bond!") 4196 4197 else: 4198 if self.moreDebug: 4199 uLogger.debug("Records about bond payment calendar successfully received") 4200 4201 return calendar 4202 4203 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4204 """ 4205 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4206 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4207 coupon yields, current yields and some statistics etc. 4208 4209 WARNING! This is too long operation if a lot of bonds requested from broker server. 4210 4211 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4212 4213 :param instruments: list of strings with tickers or FIGIs. 4214 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4215 for further used by data scientists or stock analytics. 4216 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4217 In XLSX-file and Pandas DataFrame fields mean: 4218 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4219 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4220 """ 4221 if instruments is None or not instruments: 4222 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4223 raise Exception("Ticker or FIGI required") 4224 4225 if isinstance(instruments, str): 4226 instruments = [instruments] 4227 4228 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4229 4230 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4231 4232 iCount = len(uniqueInstruments) 4233 tooLong = iCount >= 20 4234 if tooLong: 4235 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4236 4237 bonds = None 4238 for i, self._figi in enumerate(uniqueInstruments): 4239 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4240 4241 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4242 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4243 rawBond = self.SearchByFIGI(requestPrice=True) 4244 4245 # Widen raw data with UTC current time (iData["actualDateTime"]): 4246 actualDate = datetime.now(tzutc()) 4247 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4248 4249 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4250 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4251 4252 # Replace some values with human-readable: 4253 iData["nominalCurrency"] = iData["nominal"]["currency"] 4254 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4255 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4256 iData["aciCurrency"] = iData["aciValue"]["currency"] 4257 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4258 iData["issueSize"] = int(iData["issueSize"]) 4259 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4260 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4261 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4262 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4263 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4264 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4265 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4266 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4267 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4268 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4269 4270 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4271 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4272 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4273 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4274 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4275 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4276 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4277 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4278 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4279 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4280 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4281 4282 # Widen raw data with calendar data from `rawCalendar` values: 4283 calendarData = [] 4284 if "events" in iData["rawCalendar"].keys(): 4285 for item in iData["rawCalendar"]["events"]: 4286 calendarData.append({ 4287 "couponDate": item["couponDate"], 4288 "couponNumber": int(item["couponNumber"]), 4289 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4290 "payCurrency": item["payOneBond"]["currency"], 4291 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4292 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4293 "couponStartDate": item["couponStartDate"], 4294 "couponEndDate": item["couponEndDate"], 4295 "couponPeriod": item["couponPeriod"], 4296 }) 4297 4298 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4299 if "maturityDate" not in iData.keys(): 4300 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4301 4302 # Widen raw data with Coupon Rate. 4303 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4304 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4305 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4306 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4307 4308 # Widen raw data with Yield to Maturity (YTM) on current date. 4309 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4310 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4311 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4312 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4313 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4314 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4315 4316 iData["calendar"] = calendarData # adds calendar at the end 4317 4318 # Remove not used data: 4319 iData.pop("uid") 4320 iData.pop("positionUid") 4321 iData.pop("currentPrice") 4322 iData.pop("rawCalendar") 4323 4324 colNames = list(iData.keys()) 4325 if bonds is None: 4326 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4327 4328 else: 4329 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4330 4331 else: 4332 uLogger.warning("Instrument is not a bond!") 4333 4334 processed = round(100 * (i + 1) / iCount, 1) 4335 if tooLong and processed % 5 == 0: 4336 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4337 4338 else: 4339 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4340 4341 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4342 4343 # Saving bonds from Pandas DataFrame to XLSX sheet: 4344 if xlsx and self.bondsXLSXFile: 4345 with pd.ExcelWriter( 4346 path=self.bondsXLSXFile, 4347 date_format=TKS_DATE_FORMAT, 4348 datetime_format=TKS_DATE_TIME_FORMAT, 4349 mode="w", 4350 ) as writer: 4351 bonds.to_excel( 4352 writer, 4353 sheet_name="Extended bonds data", 4354 index=True, 4355 encoding="UTF-8", 4356 freeze_panes=(1, 1), 4357 ) # saving as XLSX-file with freeze first row and column as headers 4358 4359 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4360 4361 return bonds 4362 4363 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4364 """ 4365 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4366 4367 WARNING! This is too long operation if a lot of bonds requested from broker server. 4368 4369 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4370 4371 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4372 extended information about bonds: main info, current prices, bond payment calendar, 4373 coupon yields, current yields and some statistics etc. 4374 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4375 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4376 for further used by data scientists or stock analytics. 4377 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4378 """ 4379 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4380 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4381 4382 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4383 4384 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4385 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4386 calendar = None 4387 for bond in extBonds.iterrows(): 4388 for item in bond[1]["calendar"]: 4389 cData = { 4390 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4391 "couponDate": item["couponDate"], 4392 "figi": bond[1]["figi"], 4393 "ticker": bond[1]["ticker"], 4394 "name": bond[1]["name"], 4395 "couponNumber": item["couponNumber"], 4396 "payOneBond": item["payOneBond"], 4397 "payCurrency": item["payCurrency"], 4398 "couponType": item["couponType"], 4399 "couponPeriod": item["couponPeriod"], 4400 "fixDate": item["fixDate"], 4401 "couponStartDate": item["couponStartDate"], 4402 "couponEndDate": item["couponEndDate"], 4403 } 4404 4405 if calendar is None: 4406 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4407 4408 else: 4409 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4410 4411 if calendar is not None: 4412 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4413 4414 # Saving calendar from Pandas DataFrame to XLSX sheet: 4415 if xlsx: 4416 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4417 4418 with pd.ExcelWriter( 4419 path=xlsxCalendarFile, 4420 date_format=TKS_DATE_FORMAT, 4421 datetime_format=TKS_DATE_TIME_FORMAT, 4422 mode="w", 4423 ) as writer: 4424 humanReadable = calendar.copy(deep=True) 4425 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4426 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4427 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4428 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4429 humanReadable.columns = colNames # human-readable column names 4430 4431 humanReadable.to_excel( 4432 writer, 4433 sheet_name="Bond payments calendar", 4434 index=False, 4435 encoding="UTF-8", 4436 freeze_panes=(1, 2), 4437 ) # saving as XLSX-file with freeze first row and column as headers 4438 4439 del humanReadable # release df in memory 4440 4441 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4442 4443 return calendar 4444 4445 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4446 """ 4447 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4448 Also, creates Markdown file with calendar data, `calendar.md` by default. 4449 4450 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4451 4452 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4453 extended information about bonds: main info, current prices, bond payment calendar, 4454 coupon yields, current yields and some statistics etc. 4455 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4456 :param show: if `True` then also printing bonds payment calendar to the console, 4457 otherwise save to file `calendarFile` only. `False` by default. 4458 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4459 :return: multilines text in Markdown format with bonds payment calendar as a table. 4460 """ 4461 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4462 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4463 4464 infoText = "# Bond payments calendar\n\n" 4465 4466 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4467 4468 if not (calendar is None or calendar.empty): 4469 splitLine = "| | | | | | | | | |\n" 4470 4471 info = [ 4472 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4473 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4474 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4475 ] 4476 4477 newMonth = False 4478 notOneBond = calendar["figi"].nunique() > 1 4479 for i, bond in enumerate(calendar.iterrows()): 4480 if newMonth and notOneBond: 4481 info.append(splitLine) 4482 4483 info.append( 4484 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4485 " √" if bond[1]["paid"] else " —", 4486 bond[1]["couponDate"].split("T")[0], 4487 bond[1]["figi"], 4488 bond[1]["ticker"], 4489 bond[1]["couponNumber"], 4490 "{} {}".format( 4491 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4492 bond[1]["payCurrency"], 4493 ), 4494 bond[1]["couponType"], 4495 bond[1]["couponPeriod"], 4496 bond[1]["fixDate"].split("T")[0], 4497 ) 4498 ) 4499 4500 if i < len(calendar.values) - 1: 4501 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4502 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4503 newMonth = False if curDate.month == nextDate.month else True 4504 4505 else: 4506 newMonth = False 4507 4508 infoText += "".join(info) 4509 4510 if show and not onlyFiles: 4511 uLogger.info("{}".format(infoText)) 4512 4513 if self.calendarFile is not None and (show or onlyFiles): 4514 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4515 fH.write(infoText) 4516 4517 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4518 4519 if self.useHTMLReports: 4520 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4521 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4522 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4523 4524 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4525 4526 else: 4527 infoText += "No data\n" 4528 4529 return infoText 4530 4531 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4532 """ 4533 Method for parsing and show simple table with all available user accounts. 4534 4535 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4536 4537 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4538 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4539 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4540 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4541 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4542 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4543 "closed": "—", "access": "Full access" }, ...}}` 4544 """ 4545 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4546 4547 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4548 accounts = { 4549 item["id"]: { 4550 "type": TKS_ACCOUNT_TYPES[item["type"]], 4551 "name": item["name"], 4552 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4553 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4554 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4555 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4556 } for item in rawAccounts["accounts"] 4557 } 4558 4559 # Raw and parsed data with some fields replaced in "stat" section: 4560 view = { 4561 "rawAccounts": rawAccounts, 4562 "stat": accounts, 4563 } 4564 4565 # --- Prepare simple text table with only accounts data in human-readable format: 4566 if show or onlyFiles: 4567 info = [ 4568 "# User accounts\n\n", 4569 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4570 "| Account ID | Type | Status | Name |\n", 4571 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4572 ] 4573 4574 for account in view["stat"].keys(): 4575 info.extend([ 4576 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4577 account, 4578 view["stat"][account]["type"], 4579 view["stat"][account]["status"], 4580 view["stat"][account]["name"], 4581 ) 4582 ]) 4583 4584 infoText = "".join(info) 4585 4586 if show and not onlyFiles: 4587 uLogger.info(infoText) 4588 4589 if self.userAccountsFile and (show or onlyFiles): 4590 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4591 fH.write(infoText) 4592 4593 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4594 4595 if self.useHTMLReports: 4596 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4597 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4598 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4599 4600 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4601 4602 return view 4603 4604 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4605 """ 4606 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4607 4608 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4609 4610 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4611 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4612 :return: dict with raw parsed data from server and some calculated statistics about it. 4613 """ 4614 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4615 tmpTicker = self._ticker 4616 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4617 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4618 self._ticker = tmpTicker 4619 4620 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4621 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4622 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4623 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4624 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4625 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4626 4627 # This is dict with parsed common user data: 4628 userInfo = { 4629 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4630 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4631 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4632 "tariff": rawUserInfo["tariff"], 4633 } 4634 4635 # This is an array of dict with parsed margin statuses for every account IDs: 4636 margins = {} 4637 for accountId in accounts.keys(): 4638 if rawMargins[accountId]: 4639 margins[accountId] = { 4640 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4641 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4642 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4643 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4644 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4645 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4646 "missing": missing["volume"], 4647 } 4648 4649 else: 4650 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4651 4652 unary = {} # unary-connection limits 4653 for item in rawTariffLimits["unaryLimits"]: 4654 if item["limitPerMinute"] in unary.keys(): 4655 unary[item["limitPerMinute"]].extend(item["methods"]) 4656 4657 else: 4658 unary[item["limitPerMinute"]] = item["methods"] 4659 4660 stream = {} # stream-connection limits 4661 for item in rawTariffLimits["streamLimits"]: 4662 if item["limit"] in stream.keys(): 4663 stream[item["limit"]].extend(item["streams"]) 4664 4665 else: 4666 stream[item["limit"]] = item["streams"] 4667 4668 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4669 limits = { 4670 "unary": unary, 4671 "stream": stream, 4672 } 4673 4674 # Raw and parsed data as an output result: 4675 view = { 4676 "rawUserInfo": rawUserInfo, 4677 "rawAccounts": rawAccounts, 4678 "rawMargins": rawMargins, 4679 "rawTariffLimits": rawTariffLimits, 4680 "stat": { 4681 "overview": overview, 4682 "userInfo": userInfo, 4683 "accounts": accounts, 4684 "margins": margins, 4685 "limits": limits, 4686 }, 4687 } 4688 4689 # --- Prepare text table with user information in human-readable format: 4690 if show or onlyFiles: 4691 info = [ 4692 "# Full user information\n\n", 4693 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4694 "## Common information\n\n", 4695 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4696 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4697 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4698 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4699 "\n## User accounts\n\n", 4700 ] 4701 4702 for account in view["stat"]["accounts"].keys(): 4703 info.extend([ 4704 "### ID: [{}]\n\n".format(account), 4705 "| Parameters | Values |\n", 4706 "|----------------------|--------------------------------------------------------------|\n", 4707 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4708 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4709 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4710 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4711 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4712 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4713 ]) 4714 4715 if margins[account]: 4716 info.extend([ 4717 "| Margin status: | Enabled |\n", 4718 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4719 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4720 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4721 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4722 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4723 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4724 ]) 4725 4726 else: 4727 info.append("| Margin status: | Disabled |\n\n") 4728 4729 info.extend([ 4730 "\n## Current user tariff limits\n", 4731 "\n### See also\n", 4732 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4733 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4734 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4735 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4736 "\n### Unary limits\n", 4737 ]) 4738 4739 if unary: 4740 for key, values in sorted(unary.items()): 4741 info.append("\n* Max requests per minute: {}\n".format(key)) 4742 4743 for value in values: 4744 info.append(" - {}\n".format(value)) 4745 4746 else: 4747 info.append("\nNot available\n") 4748 4749 info.append("\n### Stream limits\n") 4750 4751 if stream: 4752 for key, values in sorted(stream.items()): 4753 info.append("\n* Max stream connections: {}\n".format(key)) 4754 4755 for value in values: 4756 info.append(" - {}\n".format(value)) 4757 4758 else: 4759 info.append("\nNot available\n") 4760 4761 infoText = "".join(info) 4762 4763 if show and not onlyFiles: 4764 uLogger.info(infoText) 4765 4766 if self.userInfoFile and (show or onlyFiles): 4767 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4768 fH.write(infoText) 4769 4770 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4771 4772 if self.useHTMLReports: 4773 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4774 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4775 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4776 4777 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4778 4779 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self._tag = "" 130 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 131 132 self.__lock = Lock() # initialize multiprocessing mutex lock 133 134 self._precision = 4 # precision, signs after comma, e.g. 2 for instruments like PLZL, 4 for instruments like USDRUB, if -1 then auto detect it when load data-file 135 136 self.aliases = TKS_TICKER_ALIASES 137 """Some aliases instead official tickers. 138 139 See also: `TKSEnums.TKS_TICKER_ALIASES` 140 """ 141 142 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 143 144 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 145 146 self._ticker = "" 147 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 148 149 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 150 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 151 152 See also: `SearchByTicker()`, `SearchInstruments()`. 153 """ 154 155 self._figi = "" 156 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 157 158 See also: `SearchByFIGI()`, `SearchInstruments()`. 159 """ 160 161 self.depth = 1 162 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 163 164 See also: `GetCurrentPrices()`. 165 """ 166 167 self.server = r"https://invest-public-api.tinkoff.ru/rest" 168 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 169 170 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 171 """ 172 173 uLogger.debug("Broker API server: {}".format(self.server)) 174 175 self.timeout = 15 176 """Server operations timeout in seconds. Default: `15`. 177 178 See also: `SendAPIRequest()`. 179 """ 180 181 self.headers = { 182 "Content-Type": "application/json", 183 "accept": "application/json", 184 "Authorization": "Bearer {}".format(self.token), 185 "x-app-name": "Tim55667757.TKSBrokerAPI", 186 } 187 """ 188 Headers which send in every request to broker server. Please, do not change it! 189 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 190 191 See also: `SendAPIRequest()`. 192 """ 193 194 self.body = None 195 """Request body which send to broker server. Default: `None`. 196 197 See also: `SendAPIRequest()`. 198 """ 199 200 self.moreDebug = False 201 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 202 203 self.useHTMLReports = False 204 """ 205 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 206 207 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 208 """ 209 210 self.historyFile = None 211 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 212 213 See also: `History()`. 214 """ 215 216 self.htmlHistoryFile = "index.html" 217 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 218 219 See also: `ShowHistoryChart()`. 220 """ 221 222 self.instrumentsFile = "instruments.md" 223 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 224 225 See also: `ShowInstrumentsInfo()`. 226 """ 227 228 self.searchResultsFile = "search-results.md" 229 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 230 231 See also: `SearchInstruments()`. 232 """ 233 234 self.pricesFile = "prices.md" 235 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 236 237 See also: `GetListOfPrices()`. 238 """ 239 240 self.infoFile = "info.md" 241 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 242 243 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 244 """ 245 246 self.bondsXLSXFile = "ext-bonds.xlsx" 247 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 248 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 249 250 See also: `ExtendBondsData()`. 251 """ 252 253 self.calendarFile = "calendar.md" 254 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 255 256 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 257 258 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 259 """ 260 261 self.overviewFile = "overview.md" 262 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 263 264 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 265 """ 266 267 self.overviewDigestFile = "overview-digest.md" 268 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 269 270 See also: `Overview()` with parameter `details="digest"`. 271 """ 272 273 self.overviewPositionsFile = "overview-positions.md" 274 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 275 276 See also: `Overview()` with parameter `details="positions"`. 277 """ 278 279 self.overviewOrdersFile = "overview-orders.md" 280 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 281 282 See also: `Overview()` with parameter `details="orders"`. 283 """ 284 285 self.overviewAnalyticsFile = "overview-analytics.md" 286 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 287 288 See also: `Overview()` with parameter `details="analytics"`. 289 """ 290 291 self.overviewBondsCalendarFile = "overview-calendar.md" 292 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 293 294 See also: `Overview()` with parameter `details="calendar"`. 295 """ 296 297 self.reportFile = "deals.md" 298 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 299 300 See also: `Deals()`. 301 """ 302 303 self.withdrawalLimitsFile = "limits.md" 304 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 305 306 See also: `OverviewLimits()` and `RequestLimits()`. 307 """ 308 309 self.userInfoFile = "user-info.md" 310 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 311 312 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 313 """ 314 315 self.userAccountsFile = "accounts.md" 316 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 317 318 See also: `OverviewAccounts()`, `RequestAccounts()`. 319 """ 320 321 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 322 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 323 324 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 325 326 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 327 """ 328 329 self.iList = None # init iList for raw instruments data 330 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 331 332 See also: `Listing()`, `DumpInstruments()`. 333 """ 334 335 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 336 if useCache: 337 if os.path.exists(self.iListDumpFile): 338 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 339 curTime = datetime.now(tzutc()) 340 341 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 342 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 343 344 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 345 346 else: 347 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 348 349 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 350 os.path.abspath(self.iListDumpFile), 351 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 352 )) 353 354 else: 355 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 356 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 357 358 else: 359 self.iList = self.Listing() # request new raw instruments data from broker server 360 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 361 362 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 363 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 364 365 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 366 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it!
Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
448 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 449 """ 450 Send GET or POST request to broker server and receive JSON object. 451 452 self.header: must be defining with dictionary of headers. 453 self.body: if define then used as request body. None by default. 454 self.timeout: global request timeout, 15 seconds by default. 455 :param url: url with REST request. 456 :param reqType: send "GET" or "POST" request. "GET" by default. 457 :param retry: how many times retry after first request if an 5xx server errors occurred. 458 :param pause: sleep time in seconds between retries. 459 :return: response JSON (dictionary) from broker. 460 """ 461 if reqType.upper() not in ("GET", "POST"): 462 uLogger.error("You can define request type: `GET` or `POST`!") 463 raise Exception("Incorrect value") 464 465 if self.moreDebug: 466 uLogger.debug("Request parameters:") 467 uLogger.debug(" - REST API URL: {}".format(url)) 468 uLogger.debug(" - request type: {}".format(reqType)) 469 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 470 uLogger.debug(" - body:\n{}".format(self.body)) 471 472 # fast hack to avoid all operations with some tickers/FIGI 473 responseJSON = {} 474 oK = True 475 for item in self.exclude: 476 if item in url: 477 if self.moreDebug: 478 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 479 480 oK = False 481 break 482 483 if oK: 484 with self.__lock: # acquire the mutex lock 485 counter = 0 486 response = None 487 errMsg = "" 488 489 while not response and counter <= retry: 490 if reqType == "GET": 491 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 492 493 if reqType == "POST": 494 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 495 496 if self.moreDebug: 497 uLogger.debug("Response:") 498 uLogger.debug(" - status code: {}".format(response.status_code)) 499 uLogger.debug(" - reason: {}".format(response.reason)) 500 uLogger.debug(" - body length: {}".format(len(response.text))) 501 uLogger.debug(" - headers:\n{}".format(response.headers)) 502 503 # Server returns some headers: 504 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 505 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 506 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 507 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 508 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 509 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 510 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 511 sleep(rateLimitWait) 512 513 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 514 if 400 <= response.status_code < 500: 515 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 516 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 517 518 if "code" in response.text and "message" in response.text: 519 msgDict = self._ParseJSON(rawData=response.text) 520 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 521 522 counter = retry + 1 # do not retry for 4xx errors 523 524 if 500 <= response.status_code < 600: 525 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 526 uLogger.debug(" - not oK, {}".format(errMsg)) 527 528 if "code" in response.text and "message" in response.text: 529 errMsgDict = self._ParseJSON(rawData=response.text) 530 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 531 532 counter += 1 533 534 if counter <= retry: 535 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 536 sleep(pause) 537 538 responseJSON = self._ParseJSON(rawData=response.text) 539 540 if errMsg: 541 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 542 uLogger.error(" - not oK, {}".format(errMsg)) 543 544 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
577 def Listing(self) -> dict: 578 """ 579 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 580 581 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 582 """ 583 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 584 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 585 586 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 587 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 588 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 589 590 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 591 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 592 poolUpdater.close() # close the thread pool 593 poolUpdater.join() # wait a moment until all data returns from threads 594 595 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 596 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 597 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 598 599 # calculate minimum price increment (step) for all instruments and set up instrument's type: 600 for iType in iList.keys(): 601 for ticker in iList[iType]: 602 iList[iType][ticker]["type"] = iType 603 604 if "minPriceIncrement" in iList[iType][ticker].keys(): 605 iList[iType][ticker]["step"] = NanoToFloat( 606 iList[iType][ticker]["minPriceIncrement"]["units"], 607 iList[iType][ticker]["minPriceIncrement"]["nano"], 608 ) 609 610 else: 611 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 612 613 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
615 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 616 """ 617 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 618 619 See also: `DumpInstruments()`, `Listing()`. 620 621 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 622 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 623 """ 624 if self.iListDumpFile is None or not self.iListDumpFile: 625 uLogger.error("Output name of dump file must be defined!") 626 raise Exception("Filename required") 627 628 if not self.iList or forceUpdate: 629 self.iList = self.Listing() 630 631 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 632 633 # Save as XLSX with separated sheets for every type of instruments: 634 with pd.ExcelWriter( 635 path=xlsxDumpFile, 636 date_format=TKS_DATE_FORMAT, 637 datetime_format=TKS_DATE_TIME_FORMAT, 638 mode="w", 639 ) as writer: 640 for iType in TKS_INSTRUMENTS: 641 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 642 df = df[sorted(df)] # sorted by column names 643 df = df.applymap( 644 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 645 na_action="ignore", 646 ) # converting numbers from nano-type to float in every cell 647 df.to_excel( 648 writer, 649 sheet_name=iType, 650 encoding="UTF-8", 651 freeze_panes=(1, 1), 652 ) # saving as XLSX-file with freeze first row and column as headers 653 654 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
656 def DumpInstruments(self, forceUpdate: bool = True) -> str: 657 """ 658 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 659 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 660 661 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 662 663 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 664 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 665 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 666 """ 667 if self.iListDumpFile is None or not self.iListDumpFile: 668 uLogger.error("Output name of dump file must be defined!") 669 raise Exception("Filename required") 670 671 if not self.iList or forceUpdate: 672 self.iList = self.Listing() 673 674 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 675 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 676 fH.write(jsonDump) 677 678 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 679 680 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
682 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 683 """ 684 Show information about one instrument defined by json data and prints it in Markdown format. 685 686 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 687 688 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 689 :param show: if `True` then also printing information about instrument and its current price. 690 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 691 :return: multilines text in Markdown format with information about one instrument. 692 """ 693 splitLine = "| | |\n" 694 infoText = "" 695 696 if iJSON is not None and iJSON and isinstance(iJSON, dict): 697 info = [ 698 "# Main information\n\n", 699 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 700 "| Parameters | Values |\n", 701 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 702 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 703 "| Full name: | {:<54} |\n".format(iJSON["name"]), 704 ] 705 706 if "sector" in iJSON.keys() and iJSON["sector"]: 707 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 708 709 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 710 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 711 712 info.extend([ 713 splitLine, 714 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 715 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 716 ]) 717 718 if "isin" in iJSON.keys() and iJSON["isin"]: 719 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 720 721 if "classCode" in iJSON.keys(): 722 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 723 724 info.extend([ 725 splitLine, 726 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 727 splitLine, 728 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 729 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 730 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 731 ]) 732 733 if iJSON["figi"]: 734 self._figi = iJSON["figi"] 735 iJSON = iJSON | self.RequestTradingStatus() 736 737 info.extend([ 738 splitLine, 739 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 740 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 741 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 742 ]) 743 744 info.append(splitLine) 745 746 if "type" in iJSON.keys() and iJSON["type"]: 747 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 748 749 if "shareType" in iJSON.keys() and iJSON["shareType"]: 750 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 751 752 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 753 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 754 755 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 756 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 757 758 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 759 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 760 761 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 762 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 763 764 if "focusType" in iJSON.keys() and iJSON["focusType"]: 765 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 766 767 if "assetType" in iJSON.keys() and iJSON["assetType"]: 768 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 769 770 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 771 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 772 773 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 774 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 775 776 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 777 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 778 779 if "currency" in iJSON.keys(): 780 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 781 782 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 783 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 784 785 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 786 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 787 788 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 789 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 790 791 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 792 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 793 794 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 795 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 796 797 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 798 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 799 800 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 801 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 802 803 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 804 info.append("| Perpetual bond: | Yes |\n") 805 806 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 807 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 808 809 iExt = None 810 if iJSON["type"] == "Bonds": 811 info.extend([ 812 splitLine, 813 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 814 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 815 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 816 iJSON["nominal"]["currency"], 817 )), 818 ]) 819 820 if "floatingCouponFlag" in iJSON.keys(): 821 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 822 823 if "amortizationFlag" in iJSON.keys(): 824 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 825 826 info.append(splitLine) 827 828 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 829 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 830 831 if iJSON["figi"]: 832 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 833 834 info.extend([ 835 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 836 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 837 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 838 ]) 839 840 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 841 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 842 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 843 iJSON["aciValue"]["currency"] 844 ))) 845 846 if "currentPrice" in iJSON.keys(): 847 info.append(splitLine) 848 849 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 850 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 851 852 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 853 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 854 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 855 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 856 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 857 858 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 859 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 860 861 info.extend([ 862 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 863 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 864 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 865 )), 866 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 867 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 868 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 869 )), 870 "| Changes between last deal price and last close | {:<54} |\n".format( 871 "{:.2f}%{}".format( 872 iJSON["currentPrice"]["changes"], 873 " ({}{:.2f} {})".format( 874 "+" if bondChangesDelta > 0 else "", 875 bondChangesDelta, 876 aciCurrency 877 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 878 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 879 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 880 currency 881 ), 882 ) 883 ), 884 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 885 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 886 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 887 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 888 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 889 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 890 )), 891 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 895 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 ]) 899 900 if "lot" in iJSON.keys(): 901 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 902 903 if "step" in iJSON.keys() and iJSON["step"] != 0: 904 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 905 906 # Add bond payment calendar: 907 if iJSON["type"] == "Bonds": 908 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 909 info.extend(["\n#", strCalendar]) 910 911 infoText += "".join(info) 912 913 if show and not onlyFiles: 914 uLogger.info("{}".format(infoText)) 915 916 if self.infoFile is not None and (show or onlyFiles): 917 with open(self.infoFile, "w", encoding="UTF-8") as fH: 918 fH.write(infoText) 919 920 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 921 922 if self.useHTMLReports: 923 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 924 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 925 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 926 927 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 928 929 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with information about one instrument.
931 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 932 """ 933 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 934 935 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 936 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 937 :return: JSON formatted data with information about instrument. 938 """ 939 tickerJSON = {} 940 if self.moreDebug: 941 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 942 943 if not self._ticker: 944 uLogger.warning("self._ticker variable is not be empty!") 945 946 else: 947 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 948 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 949 raise Exception("Instrument not allowed") 950 951 if not self.iList: 952 self.iList = self.Listing() 953 954 if self._ticker in self.iList["Shares"].keys(): 955 tickerJSON = self.iList["Shares"][self._ticker] 956 if self.moreDebug: 957 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 958 959 elif self._ticker in self.iList["Currencies"].keys(): 960 tickerJSON = self.iList["Currencies"][self._ticker] 961 if self.moreDebug: 962 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 963 964 elif self._ticker in self.iList["Bonds"].keys(): 965 tickerJSON = self.iList["Bonds"][self._ticker] 966 if self.moreDebug: 967 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 968 969 elif self._ticker in self.iList["Etfs"].keys(): 970 tickerJSON = self.iList["Etfs"][self._ticker] 971 if self.moreDebug: 972 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 973 974 elif self._ticker in self.iList["Futures"].keys(): 975 tickerJSON = self.iList["Futures"][self._ticker] 976 if self.moreDebug: 977 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 978 979 if tickerJSON: 980 self._figi = tickerJSON["figi"] 981 982 if requestPrice: 983 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 984 985 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 986 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 987 988 else: 989 tickerJSON["currentPrice"]["changes"] = 0 990 991 if show: 992 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 993 994 else: 995 if show: 996 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 997 998 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1000 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1001 """ 1002 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1003 1004 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1005 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1006 :return: JSON formatted data with information about instrument. 1007 """ 1008 figiJSON = {} 1009 if self.moreDebug: 1010 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1011 1012 if not self._figi: 1013 uLogger.warning("self._figi variable is not be empty!") 1014 1015 else: 1016 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1017 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1018 raise Exception("Instrument not allowed") 1019 1020 if not self.iList: 1021 self.iList = self.Listing() 1022 1023 for item in self.iList["Shares"].keys(): 1024 if self._figi == self.iList["Shares"][item]["figi"]: 1025 figiJSON = self.iList["Shares"][item] 1026 1027 if self.moreDebug: 1028 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1029 1030 break 1031 1032 if not figiJSON: 1033 for item in self.iList["Currencies"].keys(): 1034 if self._figi == self.iList["Currencies"][item]["figi"]: 1035 figiJSON = self.iList["Currencies"][item] 1036 1037 if self.moreDebug: 1038 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Bonds"].keys(): 1044 if self._figi == self.iList["Bonds"][item]["figi"]: 1045 figiJSON = self.iList["Bonds"][item] 1046 1047 if self.moreDebug: 1048 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Etfs"].keys(): 1054 if self._figi == self.iList["Etfs"][item]["figi"]: 1055 figiJSON = self.iList["Etfs"][item] 1056 1057 if self.moreDebug: 1058 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Futures"].keys(): 1064 if self._figi == self.iList["Futures"][item]["figi"]: 1065 figiJSON = self.iList["Futures"][item] 1066 1067 if self.moreDebug: 1068 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1069 1070 break 1071 1072 if figiJSON: 1073 self._figi = figiJSON["figi"] 1074 self._ticker = figiJSON["ticker"] 1075 1076 if requestPrice: 1077 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1078 1079 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1080 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1081 1082 else: 1083 figiJSON["currentPrice"]["changes"] = 0 1084 1085 if show: 1086 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1087 1088 else: 1089 if show: 1090 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1091 1092 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1094 def GetCurrentPrices(self, show: bool = True) -> dict: 1095 """ 1096 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1097 `{"buy": [{"price": 1243.8, "quantity": 193}, 1098 {"price": 1244.0, "quantity": 168}, 1099 {"price": 1244.8, "quantity": 5}, 1100 {"price": 1245.0, "quantity": 61}, 1101 {"price": 1245.4, "quantity": 60}], 1102 "sell": [{"price": 1243.6, "quantity": 8}, 1103 {"price": 1242.6, "quantity": 10}, 1104 {"price": 1242.4, "quantity": 18}, 1105 {"price": 1242.2, "quantity": 50}, 1106 {"price": 1242.0, "quantity": 113}], 1107 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1108 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1109 - sell: list of dicts with Buyers prices, 1110 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1111 - quantity: volume value by current price in lots, 1112 - limitUp: current trade session limit price, maximum, 1113 - limitDown: current trade session limit price, minimum, 1114 - lastPrice: last deal price of the instrument, 1115 - closePrice: previous trade session close price of the instrument. 1116 1117 See also: `SearchByTicker()` and `SearchByFIGI()`. 1118 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1119 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1120 1121 :param show: if `True` then print DOM to log and console. 1122 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1123 If an error occurred then returns an empty record: 1124 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1125 """ 1126 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1127 1128 if self.depth < 1: 1129 uLogger.error("Depth of Market (DOM) must be >=1!") 1130 raise Exception("Incorrect value") 1131 1132 if not (self._ticker or self._figi): 1133 uLogger.error("self._ticker or self._figi variables must be defined!") 1134 raise Exception("Ticker or FIGI required") 1135 1136 if self._ticker and not self._figi: 1137 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1138 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1139 1140 if not self._ticker and self._figi: 1141 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1142 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1143 1144 if not self._figi: 1145 uLogger.error("FIGI is not defined!") 1146 raise Exception("Ticker or FIGI required") 1147 1148 else: 1149 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1150 1151 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1152 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1153 self.body = str({"figi": self._figi, "depth": self.depth}) 1154 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1155 1156 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1157 # list of dicts with sellers orders: 1158 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1159 1160 # list of dicts with buyers orders: 1161 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1162 1163 # max price of instrument at this time: 1164 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1165 1166 # min price of instrument at this time: 1167 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1168 1169 # last price of deal with instrument: 1170 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1171 1172 # last close price of instrument: 1173 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1174 1175 else: 1176 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1177 uLogger.debug("Server response: {}".format(pricesResponse)) 1178 1179 if show: 1180 if prices["buy"] or prices["sell"]: 1181 info = [ 1182 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1183 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1184 self._ticker, 1185 self._figi, 1186 self.depth, 1187 ), 1188 "-" * 60, "\n", 1189 " Orders of Buyers | Orders of Sellers\n", 1190 "-" * 60, "\n", 1191 " Sell prices (volumes) | Buy prices (volumes)\n", 1192 "-" * 60, "\n", 1193 ] 1194 1195 if not prices["buy"]: 1196 info.append(" | No orders!\n") 1197 sumBuy = 0 1198 1199 else: 1200 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1201 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1202 for item in maxMinSorted: 1203 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1204 1205 if not prices["sell"]: 1206 info.append("No orders! |\n") 1207 sumSell = 0 1208 1209 else: 1210 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1211 for item in prices["sell"]: 1212 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1213 1214 info.extend([ 1215 "-" * 60, "\n", 1216 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1217 "-" * 60, "\n", 1218 ]) 1219 1220 infoText = "".join(info) 1221 1222 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1223 1224 else: 1225 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1226 1227 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1229 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1230 """ 1231 This method get and show information about all available broker instruments for current user account. 1232 If `instrumentsFile` string is not empty then also save information to this file. 1233 1234 :param show: if `True` then print results to console, if `False` — print only to file. 1235 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1236 :return: multi-lines string with all available broker instruments. 1237 """ 1238 if not self.iList: 1239 self.iList = self.Listing() 1240 1241 info = [ 1242 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1243 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1244 ] 1245 1246 # add instruments count by type: 1247 for iType in self.iList.keys(): 1248 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1249 1250 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1251 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1252 1253 # generating info tables with all instruments by type: 1254 for iType in self.iList.keys(): 1255 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1256 1257 for instrument in self.iList[iType].keys(): 1258 iName = self.iList[iType][instrument]["name"] # instrument's name 1259 if len(iName) > 57: 1260 iName = "{}...".format(iName[:54]) # right trim for a long string 1261 1262 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1263 self.iList[iType][instrument]["ticker"], 1264 iName, 1265 self.iList[iType][instrument]["figi"], 1266 self.iList[iType][instrument]["currency"], 1267 self.iList[iType][instrument]["lot"], 1268 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1269 )) 1270 1271 infoText = "".join(info) 1272 1273 if show and not onlyFiles: 1274 uLogger.info(infoText) 1275 1276 if self.instrumentsFile and (show or onlyFiles): 1277 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1278 fH.write(infoText) 1279 1280 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1281 1282 if self.useHTMLReports: 1283 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1284 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1285 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1286 1287 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1288 1289 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multi-lines string with all available broker instruments.
1291 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1292 """ 1293 This method search and show information about instruments by part of its ticker, FIGI or name. 1294 If `searchResultsFile` string is not empty then also save information to this file. 1295 1296 :param pattern: string with part of ticker, FIGI or instrument's name. 1297 :param show: if `True` then print results to console, if `False` — return list of result only. 1298 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show and not onlyFiles: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile and (show or onlyFiles): 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 if self.useHTMLReports: 1371 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1372 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1373 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1374 1375 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1376 1377 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of dictionaries with all found instruments.
1379 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1380 """ 1381 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1382 1383 :param instruments: list of strings with tickers or FIGIs. 1384 :return: list with unique instrument FIGIs only. 1385 """ 1386 requestedInstruments = [] 1387 for iName in instruments: 1388 if iName not in self.aliases.keys(): 1389 if iName not in requestedInstruments: 1390 requestedInstruments.append(iName) 1391 1392 else: 1393 if iName not in requestedInstruments: 1394 if self.aliases[iName] not in requestedInstruments: 1395 requestedInstruments.append(self.aliases[iName]) 1396 1397 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1398 1399 onlyUniqueFIGIs = [] 1400 for iName in requestedInstruments: 1401 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1402 continue 1403 1404 self._ticker = iName 1405 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1406 1407 if not iData: 1408 self._ticker = "" 1409 self._figi = iName 1410 1411 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1412 1413 if not iData: 1414 self._figi = "" 1415 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1416 1417 if iData and iData["figi"] not in onlyUniqueFIGIs: 1418 onlyUniqueFIGIs.append(iData["figi"]) 1419 1420 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1421 1422 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1424 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1425 """ 1426 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1427 1428 See limits: https://tinkoff.github.io/investAPI/limits/ 1429 1430 If `pricesFile` string is not empty then also save information to this file. 1431 1432 :param instruments: list of strings with tickers or FIGIs. 1433 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1434 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1435 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1436 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1437 """ 1438 if instruments is None or not instruments: 1439 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1440 raise Exception("Ticker or FIGI required") 1441 1442 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1443 1444 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1445 1446 iList = [] # trying to get info and current prices about all unique instruments: 1447 for self._figi in onlyUniqueFIGIs: 1448 iData = self.SearchByFIGI(requestPrice=True, show=False) 1449 iList.append(iData) 1450 1451 self.ShowListOfPrices(iList, show, onlyFiles) 1452 1453 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1455 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1456 """ 1457 Show table contains current prices of given instruments. 1458 1459 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1460 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1461 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1462 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1463 :return: multilines text in Markdown format as a table contains current prices. 1464 """ 1465 infoText = "" 1466 1467 if show or self.pricesFile or onlyFiles: 1468 info = [ 1469 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1470 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1471 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1472 ] 1473 1474 for item in iList: 1475 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1476 item["ticker"], 1477 item["figi"], 1478 item["type"], 1479 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1480 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1481 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1482 "{} / {}".format( 1483 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1484 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1485 ), 1486 "{} / {}".format( 1487 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1488 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1489 ), 1490 item["currency"], 1491 )) 1492 1493 infoText = "".join(info) 1494 1495 if show and not onlyFiles: 1496 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1497 1498 if self.pricesFile and (show or onlyFiles): 1499 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1500 fH.write(infoText) 1501 1502 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1503 1504 if self.useHTMLReports: 1505 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1506 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1507 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1508 1509 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1510 1511 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format as a table contains current prices.
1513 def RequestTradingStatus(self) -> dict: 1514 """ 1515 Requesting trading status for the instrument defined by `figi` variable. 1516 1517 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1518 1519 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1520 1521 :return: dictionary with trading status attributes. Response example: 1522 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1523 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1524 """ 1525 if self._figi is None or not self._figi: 1526 uLogger.error("Variable `figi` must be defined for using this method!") 1527 raise Exception("FIGI required") 1528 1529 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1530 1531 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1532 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1533 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1534 1535 if self.moreDebug: 1536 uLogger.debug("Records about current trading status successfully received") 1537 1538 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1540 def RequestPortfolio(self) -> dict: 1541 """ 1542 Requesting actual user's portfolio for current `accountId`. 1543 1544 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1545 1546 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1547 1548 :return: dictionary with user's portfolio. 1549 """ 1550 if self.accountId is None or not self.accountId: 1551 uLogger.error("Variable `accountId` must be defined for using this method!") 1552 raise Exception("Account ID required") 1553 1554 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1555 1556 self.body = str({"accountId": self.accountId}) 1557 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1558 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1559 1560 if self.moreDebug: 1561 uLogger.debug("Records about user's portfolio successfully received") 1562 1563 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1565 def RequestPositions(self) -> dict: 1566 """ 1567 Requesting open positions by currencies and instruments for current `accountId`. 1568 1569 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1570 1571 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1572 1573 :return: dictionary with open positions by instruments. 1574 """ 1575 if self.accountId is None or not self.accountId: 1576 uLogger.error("Variable `accountId` must be defined for using this method!") 1577 raise Exception("Account ID required") 1578 1579 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1580 1581 self.body = str({"accountId": self.accountId}) 1582 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1583 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1584 1585 if self.moreDebug: 1586 uLogger.debug("Records about current open positions successfully received") 1587 1588 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1590 def RequestPendingOrders(self) -> list: 1591 """ 1592 Requesting current actual pending limit orders for current `accountId`. 1593 1594 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1595 1596 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1597 1598 :return: list of dictionaries with pending limit orders. 1599 """ 1600 if self.accountId is None or not self.accountId: 1601 uLogger.error("Variable `accountId` must be defined for using this method!") 1602 raise Exception("Account ID required") 1603 1604 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1605 1606 self.body = str({"accountId": self.accountId}) 1607 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1608 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1609 1610 if "orders" in rawResponse.keys(): 1611 rawOrders = rawResponse["orders"] 1612 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1613 1614 else: 1615 rawOrders = [] 1616 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1617 1618 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1620 def RequestStopOrders(self) -> list: 1621 """ 1622 Requesting current actual stop orders for current `accountId`. 1623 1624 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1625 1626 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1627 1628 :return: list of dictionaries with stop orders. 1629 """ 1630 if self.accountId is None or not self.accountId: 1631 uLogger.error("Variable `accountId` must be defined for using this method!") 1632 raise Exception("Account ID required") 1633 1634 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1635 1636 self.body = str({"accountId": self.accountId}) 1637 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1638 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1639 1640 if "stopOrders" in rawResponse.keys(): 1641 rawStopOrders = rawResponse["stopOrders"] 1642 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1643 1644 else: 1645 rawStopOrders = [] 1646 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1647 1648 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1650 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1651 """ 1652 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1653 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1654 and `overviewBondsCalendarFile` are defined then also save information to file. 1655 1656 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1657 many requests about the state of the portfolio, and then, based on the received data, a large number 1658 of calculation and statistics are collected. 1659 1660 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1661 :param details: how detailed should the information be? 1662 - `full` — shows full available information about portfolio status (by default), 1663 - `positions` — shows only open positions, 1664 - `orders` — shows only sections of open limits and stop orders. 1665 - `digest` — show a short digest of the portfolio status, 1666 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1667 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1668 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1669 :return: dictionary with client's raw portfolio and some statistics. 1670 """ 1671 if self.accountId is None or not self.accountId: 1672 uLogger.error("Variable `accountId` must be defined for using this method!") 1673 raise Exception("Account ID required") 1674 1675 view = { 1676 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1677 "headers": {}, # list of dictionaries, response headers without "positions" section 1678 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1679 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1680 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1681 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1682 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1683 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1684 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1685 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1686 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1687 }, 1688 "stat": { # --- some statistics calculated using "raw" sections: 1689 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1690 "availableRUB": 0., # available rubles (without other currencies) 1691 "blockedRUB": 0., # blocked sum in Russian Rouble 1692 "totalChangesRUB": 0., # changes for all open trades in RUB 1693 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1694 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1695 "sharesCostRUB": 0., # costs of all shares in RUB 1696 "bondsCostRUB": 0., # costs of all bonds in RUB 1697 "etfsCostRUB": 0., # costs of all etfs in RUB 1698 "futuresCostRUB": 0., # costs of all futures in RUB 1699 "Currencies": [], # list of dictionaries of all currencies statistics 1700 "Shares": [], # list of dictionaries of all shares statistics 1701 "Bonds": [], # list of dictionaries of all bonds statistics 1702 "Etfs": [], # list of dictionaries of all etfs statistics 1703 "Futures": [], # list of dictionaries of all futures statistics 1704 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1705 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1706 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1707 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1708 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1709 }, 1710 "analytics": { # --- some analytics of portfolio: 1711 "distrByAssets": {}, # portfolio distribution by assets 1712 "distrByCompanies": {}, # portfolio distribution by companies 1713 "distrBySectors": {}, # portfolio distribution by sectors 1714 "distrByCurrencies": {}, # portfolio distribution by currencies 1715 "distrByCountries": {}, # portfolio distribution by countries 1716 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1717 } 1718 } 1719 1720 details = details.lower() 1721 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1722 if details not in availableDetails: 1723 details = "full" 1724 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1725 1726 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1727 1728 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1729 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1730 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1731 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1732 1733 # save response headers without "positions" section: 1734 for key in portfolioResponse.keys(): 1735 if key != "positions": 1736 view["raw"]["headers"][key] = portfolioResponse[key] 1737 1738 else: 1739 continue 1740 1741 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1742 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1743 for item in portfolioResponse["positions"]: 1744 if item["instrumentType"] == "currency": 1745 self._figi = item["figi"] 1746 if not self._figi and item["ticker"]: 1747 self._ticker = item["ticker"] 1748 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1749 1750 curr = self.SearchByFIGI(requestPrice=False) 1751 1752 # current price of currency in RUB: 1753 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1754 "name": curr["name"], 1755 "currentPrice": NanoToFloat( 1756 item["currentPrice"]["units"], 1757 item["currentPrice"]["nano"] 1758 ), 1759 } 1760 1761 view["raw"]["Currencies"].append(item) 1762 1763 elif item["instrumentType"] == "share": 1764 view["raw"]["Shares"].append(item) 1765 1766 elif item["instrumentType"] == "bond": 1767 view["raw"]["Bonds"].append(item) 1768 1769 elif item["instrumentType"] == "etf": 1770 view["raw"]["Etfs"].append(item) 1771 1772 elif item["instrumentType"] == "futures": 1773 view["raw"]["Futures"].append(item) 1774 1775 else: 1776 continue 1777 1778 # how many volume of currencies (by ISO currency name) are blocked: 1779 for item in view["raw"]["positions"]["blocked"]: 1780 blocked = NanoToFloat(item["units"], item["nano"]) 1781 if blocked > 0: 1782 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1783 1784 # how many volume of instruments (by FIGI) are blocked: 1785 for item in view["raw"]["positions"]["securities"]: 1786 blocked = int(item["blocked"]) 1787 if blocked > 0: 1788 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1789 1790 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1791 1792 if "rub" in allBlocked.keys(): 1793 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1794 1795 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1796 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1797 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1798 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1799 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1800 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1801 view["stat"]["portfolioCostRUB"] = sum([ 1802 view["stat"]["allCurrenciesCostRUB"], 1803 view["stat"]["sharesCostRUB"], 1804 view["stat"]["bondsCostRUB"], 1805 view["stat"]["etfsCostRUB"], 1806 view["stat"]["futuresCostRUB"], 1807 ]) 1808 1809 # --- calculating some portfolio statistics: 1810 byComp = {} # distribution by companies 1811 bySect = {} # distribution by sectors 1812 byCurr = {} # distribution by currencies (include RUB) 1813 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1814 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1815 1816 for item in portfolioResponse["positions"]: 1817 self._figi = item["figi"] 1818 if not self._figi and item["ticker"]: 1819 self._ticker = item["ticker"] 1820 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1821 1822 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1823 1824 if instrument: 1825 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1826 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1827 1828 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1829 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1830 1831 else: 1832 blocked = 0 1833 1834 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1835 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1836 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1837 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1838 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1839 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1840 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1841 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1842 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1843 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1844 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1845 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1846 1847 statData = { 1848 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1849 "ticker": instrument["ticker"], # ticker by FIGI 1850 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1851 "volume": volume, # available volume of instrument 1852 "lots": lots, # volume in lots of instrument 1853 "direction": direction, # direction of an instrument's position: short or long 1854 "blocked": blocked, # blocked volume of currency or instrument 1855 "currentPrice": curPrice, # current instrument's price in basic asset 1856 "average": average, # current average position price 1857 "cost": cost, # current cost of all volume of instrument in basic asset 1858 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1859 "costRUB": costRUB, # cost of instrument in ruble 1860 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1861 "profit": profit, # expected profit at current moment 1862 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1863 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1864 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1865 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1866 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1867 "step": instrument["step"], # minimum price increment 1868 } 1869 1870 # adding distribution by unique countries: 1871 if statData["country"] not in byCountry.keys(): 1872 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1873 1874 else: 1875 byCountry[statData["country"]]["cost"] += costRUB 1876 byCountry[statData["country"]]["percent"] += percentCostRUB 1877 1878 if item["instrumentType"] != "currency": 1879 # adding distribution by unique companies: 1880 if statData["name"]: 1881 if statData["name"] not in byComp.keys(): 1882 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1883 1884 else: 1885 byComp[statData["name"]]["cost"] += costRUB 1886 byComp[statData["name"]]["percent"] += percentCostRUB 1887 1888 # adding distribution by unique sectors: 1889 if statData["sector"] not in bySect.keys(): 1890 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1891 1892 else: 1893 bySect[statData["sector"]]["cost"] += costRUB 1894 bySect[statData["sector"]]["percent"] += percentCostRUB 1895 1896 # adding distribution by unique currencies: 1897 if currency not in byCurr.keys(): 1898 byCurr[currency] = { 1899 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1900 "cost": costRUB, 1901 "percent": percentCostRUB 1902 } 1903 1904 else: 1905 byCurr[currency]["cost"] += costRUB 1906 byCurr[currency]["percent"] += percentCostRUB 1907 1908 # saving statistics for every instrument: 1909 if item["instrumentType"] == "currency": 1910 view["stat"]["Currencies"].append(statData) 1911 1912 # update dict with free funds for trading (total - blocked) by currencies 1913 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1914 view["stat"]["funds"][currency] = { 1915 "total": volume, 1916 "totalCostRUB": costRUB, # total volume cost in rubles 1917 "free": volume - blocked, 1918 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1919 } 1920 1921 elif item["instrumentType"] == "share": 1922 view["stat"]["Shares"].append(statData) 1923 1924 elif item["instrumentType"] == "bond": 1925 view["stat"]["Bonds"].append(statData) 1926 1927 elif item["instrumentType"] == "etf": 1928 view["stat"]["Etfs"].append(statData) 1929 1930 elif item["instrumentType"] == "Futures": 1931 view["stat"]["Futures"].append(statData) 1932 1933 else: 1934 continue 1935 1936 # total changes in Russian Ruble: 1937 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1938 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1939 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1940 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1941 view["stat"]["funds"]["rub"] = { 1942 "total": view["stat"]["availableRUB"], 1943 "totalCostRUB": view["stat"]["availableRUB"], 1944 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1945 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1946 } 1947 1948 # --- pending limit orders sector data: 1949 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1950 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1951 1952 for item in view["raw"]["orders"]: 1953 self._figi = item["figi"] 1954 1955 if item["figi"] not in uniquePendingOrdersFIGIs: 1956 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1957 1958 uniquePendingOrdersFIGIs.append(item["figi"]) 1959 uniquePendingOrders[item["figi"]] = instrument 1960 1961 else: 1962 instrument = uniquePendingOrders[item["figi"]] 1963 1964 if instrument: 1965 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1966 orderType = TKS_ORDER_TYPES[item["orderType"]] 1967 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1968 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1969 1970 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1971 if item["direction"] == "ORDER_DIRECTION_BUY": 1972 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1973 1974 else: 1975 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1976 1977 # requested price for order execution: 1978 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1979 1980 # necessary changes in percent to reach target from current price: 1981 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1982 1983 view["stat"]["orders"].append({ 1984 "orderID": item["orderId"], # orderId number parameter of current order 1985 "figi": item["figi"], # FIGI identification 1986 "ticker": instrument["ticker"], # ticker name by FIGI 1987 "lotsRequested": item["lotsRequested"], # requested lots value 1988 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for order execution in base currency 1991 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1992 "percentChanges": changes, # changes in percent to target from current price 1993 "currency": item["currency"], # instrument's currency name 1994 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1995 "type": orderType, # type of order from TKS_ORDER_TYPES 1996 "status": orderState, # order status from TKS_ORDER_STATES 1997 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1998 }) 1999 2000 # --- stop orders sector data: 2001 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2002 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2003 2004 for item in view["raw"]["stopOrders"]: 2005 self._figi = item["figi"] 2006 2007 if item["figi"] not in uniqueStopOrdersFIGIs: 2008 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2009 2010 uniqueStopOrdersFIGIs.append(item["figi"]) 2011 uniqueStopOrders[item["figi"]] = instrument 2012 2013 else: 2014 instrument = uniqueStopOrders[item["figi"]] 2015 2016 if instrument: 2017 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2018 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2019 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2020 2021 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2022 if "expirationTime" in item.keys(): 2023 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2024 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2025 2026 else: 2027 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2028 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2029 2030 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2031 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2032 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2033 2034 else: 2035 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2036 2037 # requested price when stop-order executed: 2038 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2039 2040 # price for limit-order, set up when stop-order executed: 2041 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2042 2043 # necessary changes in percent to reach target from current price: 2044 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2045 2046 view["stat"]["stopOrders"].append({ 2047 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2048 "figi": item["figi"], # FIGI identification 2049 "ticker": instrument["ticker"], # ticker name by FIGI 2050 "lotsRequested": item["lotsRequested"], # requested lots value 2051 "currentPrice": lastPrice, # current instrument's price for defined action 2052 "targetPrice": target, # requested price for stop-order execution in base currency 2053 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2054 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2055 "percentChanges": changes, # changes in percent to target from current price 2056 "currency": item["currency"], # instrument's currency name 2057 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2058 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2059 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2060 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2061 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2062 }) 2063 2064 # --- calculating data for analytics section: 2065 # portfolio distribution by assets: 2066 view["analytics"]["distrByAssets"] = { 2067 "Ruble": { 2068 "uniques": 1, 2069 "cost": view["stat"]["availableRUB"], 2070 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2071 }, 2072 "Currencies": { 2073 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2074 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2075 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2076 }, 2077 "Shares": { 2078 "uniques": len(view["stat"]["Shares"]), 2079 "cost": view["stat"]["sharesCostRUB"], 2080 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2081 }, 2082 "Bonds": { 2083 "uniques": len(view["stat"]["Bonds"]), 2084 "cost": view["stat"]["bondsCostRUB"], 2085 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2086 }, 2087 "Etfs": { 2088 "uniques": len(view["stat"]["Etfs"]), 2089 "cost": view["stat"]["etfsCostRUB"], 2090 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2091 }, 2092 "Futures": { 2093 "uniques": len(view["stat"]["Futures"]), 2094 "cost": view["stat"]["futuresCostRUB"], 2095 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2096 }, 2097 } 2098 2099 # portfolio distribution by companies: 2100 view["analytics"]["distrByCompanies"]["All money cash"] = { 2101 "ticker": "", 2102 "cost": view["stat"]["allCurrenciesCostRUB"], 2103 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2104 } 2105 view["analytics"]["distrByCompanies"].update(byComp) 2106 2107 # portfolio distribution by sectors: 2108 view["analytics"]["distrBySectors"]["All money cash"] = { 2109 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2110 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2111 } 2112 view["analytics"]["distrBySectors"].update(bySect) 2113 2114 # portfolio distribution by currencies: 2115 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2116 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2117 2118 if self.moreDebug: 2119 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2120 2121 view["analytics"]["distrByCurrencies"].update(byCurr) 2122 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2123 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2124 2125 # portfolio distribution by countries: 2126 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2127 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2128 2129 if self.moreDebug: 2130 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2131 2132 view["analytics"]["distrByCountries"].update(byCountry) 2133 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2134 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2135 2136 # --- Prepare text statistics overview in human-readable: 2137 if show or onlyFiles: 2138 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2139 2140 # Whatever the value `details`, header not changes: 2141 info = [ 2142 "# Client's portfolio\n\n", 2143 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2144 "* **Account ID:** [{}]\n".format(self.accountId), 2145 ] 2146 2147 if details in ["full", "positions", "digest"]: 2148 info.extend([ 2149 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2150 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2151 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2152 view["stat"]["totalChangesRUB"], 2153 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2154 view["stat"]["totalChangesPercentRUB"], 2155 ), 2156 ]) 2157 2158 if details in ["full", "positions"]: 2159 info.extend([ 2160 "## Open positions\n\n", 2161 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2162 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2163 "| **Ruble:** | {:>31} | | | | | |\n".format( 2164 "{:.2f} ({:.2f}) rub".format( 2165 view["stat"]["availableRUB"], 2166 view["stat"]["blockedRUB"], 2167 ) 2168 ) 2169 ]) 2170 2171 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2172 return [ 2173 "| | | | | | | |\n", 2174 "| {:<27} | | | | | {:>19} | |\n".format( 2175 noTradeStr if noTradeStr else typeStr, 2176 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2177 ), 2178 ] 2179 2180 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2181 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2182 "{} [{}]".format(data["ticker"], data["figi"]), 2183 "{:.2f} ({:.2f}) {}".format( 2184 data["volume"], 2185 data["blocked"], 2186 data["currency"], 2187 ) if isCurr else "{:.0f} ({:.0f})".format( 2188 data["volume"], 2189 data["blocked"], 2190 ), 2191 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2192 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2193 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2194 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2195 "{}{:.2f} {} ({}{:.2f}%)".format( 2196 "+" if data["profit"] > 0 else "", 2197 data["profit"], data["baseCurrencyName"], 2198 "+" if data["percentProfit"] > 0 else "", 2199 data["percentProfit"], 2200 ), 2201 ) 2202 2203 # --- Show currencies section: 2204 if view["stat"]["Currencies"]: 2205 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2206 for item in view["stat"]["Currencies"]: 2207 info.append(_InfoStr(item, isCurr=True)) 2208 2209 else: 2210 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2211 2212 # --- Show shares section: 2213 if view["stat"]["Shares"]: 2214 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2215 2216 for item in view["stat"]["Shares"]: 2217 info.append(_InfoStr(item)) 2218 2219 else: 2220 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2221 2222 # --- Show bonds section: 2223 if view["stat"]["Bonds"]: 2224 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2225 2226 for item in view["stat"]["Bonds"]: 2227 info.append(_InfoStr(item)) 2228 2229 else: 2230 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2231 2232 # --- Show etfs section: 2233 if view["stat"]["Etfs"]: 2234 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2235 2236 for item in view["stat"]["Etfs"]: 2237 info.append(_InfoStr(item)) 2238 2239 else: 2240 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2241 2242 # --- Show futures section: 2243 if view["stat"]["Futures"]: 2244 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2245 2246 for item in view["stat"]["Futures"]: 2247 info.append(_InfoStr(item)) 2248 2249 else: 2250 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2251 2252 if details in ["full", "orders"]: 2253 # --- Show pending limit orders section: 2254 if view["stat"]["orders"]: 2255 info.extend([ 2256 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2257 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2258 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2259 ]) 2260 2261 for item in view["stat"]["orders"]: 2262 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2263 "{} [{}]".format(item["ticker"], item["figi"]), 2264 item["orderID"], 2265 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2266 "{} {} ({}{:.2f}%)".format( 2267 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2268 item["baseCurrencyName"], 2269 "+" if item["percentChanges"] > 0 else "", 2270 float(item["percentChanges"]), 2271 ), 2272 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2273 item["action"], 2274 item["type"], 2275 item["date"], 2276 )) 2277 2278 else: 2279 info.append("\n## Total pending limit-orders: [0]\n") 2280 2281 # --- Show stop orders section: 2282 if view["stat"]["stopOrders"]: 2283 info.extend([ 2284 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2285 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2286 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2287 ]) 2288 2289 for item in view["stat"]["stopOrders"]: 2290 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2291 "{} [{}]".format(item["ticker"], item["figi"]), 2292 item["orderID"], 2293 item["lotsRequested"], 2294 "{} {} ({}{:.2f}%)".format( 2295 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2296 item["baseCurrencyName"], 2297 "+" if item["percentChanges"] > 0 else "", 2298 float(item["percentChanges"]), 2299 ), 2300 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2301 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2302 item["action"], 2303 item["type"], 2304 item["expType"], 2305 item["createDate"], 2306 item["expDate"], 2307 )) 2308 2309 else: 2310 info.append("\n## Total stop-orders: [0]\n") 2311 2312 if details in ["full", "analytics"]: 2313 # -- Show analytics section: 2314 if view["stat"]["portfolioCostRUB"] > 0: 2315 info.extend([ 2316 "\n# Analytics\n\n" 2317 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2318 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2319 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2320 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2321 view["stat"]["totalChangesRUB"], 2322 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2323 view["stat"]["totalChangesPercentRUB"], 2324 ), 2325 "\n## Portfolio distribution by assets\n" 2326 "\n| Type | Uniques | Percent | Current cost |\n", 2327 "|------------------------------------|---------|---------|--------------------|\n", 2328 ]) 2329 2330 for key in view["analytics"]["distrByAssets"].keys(): 2331 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2332 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2333 key, 2334 view["analytics"]["distrByAssets"][key]["uniques"], 2335 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2336 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2337 )) 2338 2339 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2340 2341 info.extend([ 2342 "\n## Portfolio distribution by companies\n" 2343 "\n| Company | Percent | Current cost |\n", 2344 aSepLine, 2345 ]) 2346 2347 for company in view["analytics"]["distrByCompanies"].keys(): 2348 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2349 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2350 "{}{}".format( 2351 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2352 company, 2353 ), 2354 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2355 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2356 )) 2357 2358 info.extend([ 2359 "\n## Portfolio distribution by sectors\n" 2360 "\n| Sector | Percent | Current cost |\n", 2361 aSepLine, 2362 ]) 2363 2364 for sector in view["analytics"]["distrBySectors"].keys(): 2365 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2366 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2367 sector, 2368 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2369 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2370 )) 2371 2372 info.extend([ 2373 "\n## Portfolio distribution by currencies\n" 2374 "\n| Instruments currencies | Percent | Current cost |\n", 2375 aSepLine, 2376 ]) 2377 2378 for curr in view["analytics"]["distrByCurrencies"].keys(): 2379 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2380 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2381 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2382 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2383 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2384 )) 2385 2386 info.extend([ 2387 "\n## Portfolio distribution by countries\n" 2388 "\n| Assets by country | Percent | Current cost |\n", 2389 aSepLine, 2390 ]) 2391 2392 for country in view["analytics"]["distrByCountries"].keys(): 2393 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2394 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2395 country, 2396 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2397 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2398 )) 2399 2400 if details in ["full", "calendar"]: 2401 # -- Show bonds payment calendar section: 2402 if view["stat"]["Bonds"]: 2403 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2404 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2405 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2406 2407 else: 2408 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2409 2410 infoText = "".join(info) 2411 2412 if show and not onlyFiles: 2413 uLogger.info(infoText) 2414 2415 if details == "full" and self.overviewFile: 2416 filename = self.overviewFile 2417 2418 elif details == "digest" and self.overviewDigestFile: 2419 filename = self.overviewDigestFile 2420 2421 elif details == "positions" and self.overviewPositionsFile: 2422 filename = self.overviewPositionsFile 2423 2424 elif details == "orders" and self.overviewOrdersFile: 2425 filename = self.overviewOrdersFile 2426 2427 elif details == "analytics" and self.overviewAnalyticsFile: 2428 filename = self.overviewAnalyticsFile 2429 2430 elif details == "calendar" and self.overviewBondsCalendarFile: 2431 filename = self.overviewBondsCalendarFile 2432 2433 else: 2434 filename = "" 2435 2436 if filename and (show or onlyFiles): 2437 with open(filename, "w", encoding="UTF-8") as fH: 2438 fH.write(infoText) 2439 2440 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2441 2442 if self.useHTMLReports: 2443 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2444 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2445 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2446 2447 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2448 2449 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio).
- onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dictionary with client's raw portfolio and some statistics.
2451 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2452 """ 2453 Returns history operations between two given dates for current `accountId`. 2454 If `reportFile` string is not empty then also save human-readable report. 2455 Shows some statistical data of closed positions. 2456 2457 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2458 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2459 :param show: if `True` then also prints all records to the console. 2460 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2461 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2462 :return: original list of dictionaries with history of deals records from API ("operations" key): 2463 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2464 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2465 """ 2466 if self.accountId is None or not self.accountId: 2467 uLogger.error("Variable `accountId` must be defined for using this method!") 2468 raise Exception("Account ID required") 2469 2470 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2471 2472 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2473 2474 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2475 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2476 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2477 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2478 customStat = {} # custom statistics in additional to responseJSON 2479 2480 # --- output report in human-readable format: 2481 if self.reportFile and (show or onlyFiles): 2482 splitLine1 = "| | | | | |\n" # Summary section 2483 splitLine2 = "| | | | | | | | |\n" # Operations section 2484 nextDay = "" 2485 2486 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2487 2488 if len(ops) > 0: 2489 customStat = { 2490 "opsCount": 0, # total operations count 2491 "buyCount": 0, # buy operations 2492 "sellCount": 0, # sell operations 2493 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2494 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2495 "payIn": {"rub": 0.}, # Deposit brokerage account 2496 "payOut": {"rub": 0.}, # Withdrawals 2497 "divs": {"rub": 0.}, # Dividends income 2498 "coupons": {"rub": 0.}, # Coupon's income 2499 "brokerCom": {"rub": 0.}, # Service commissions 2500 "serviceCom": {"rub": 0.}, # Service commissions 2501 "marginCom": {"rub": 0.}, # Margin commissions 2502 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2503 } 2504 2505 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2506 for item in ops: 2507 if item["state"] == "OPERATION_STATE_EXECUTED": 2508 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2509 2510 # count buy operations: 2511 if "_BUY" in item["operationType"]: 2512 customStat["buyCount"] += 1 2513 2514 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2515 customStat["buyTotal"][item["payment"]["currency"]] += payment 2516 2517 else: 2518 customStat["buyTotal"][item["payment"]["currency"]] = payment 2519 2520 # count sell operations: 2521 elif "_SELL" in item["operationType"]: 2522 customStat["sellCount"] += 1 2523 2524 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2525 customStat["sellTotal"][item["payment"]["currency"]] += payment 2526 2527 else: 2528 customStat["sellTotal"][item["payment"]["currency"]] = payment 2529 2530 # count incoming operations: 2531 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2532 if item["payment"]["currency"] in customStat["payIn"].keys(): 2533 customStat["payIn"][item["payment"]["currency"]] += payment 2534 2535 else: 2536 customStat["payIn"][item["payment"]["currency"]] = payment 2537 2538 # count withdrawals operations: 2539 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2540 if item["payment"]["currency"] in customStat["payOut"].keys(): 2541 customStat["payOut"][item["payment"]["currency"]] += payment 2542 2543 else: 2544 customStat["payOut"][item["payment"]["currency"]] = payment 2545 2546 # count dividends income: 2547 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2548 if item["payment"]["currency"] in customStat["divs"].keys(): 2549 customStat["divs"][item["payment"]["currency"]] += payment 2550 2551 else: 2552 customStat["divs"][item["payment"]["currency"]] = payment 2553 2554 # count coupon's income: 2555 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2556 if item["payment"]["currency"] in customStat["coupons"].keys(): 2557 customStat["coupons"][item["payment"]["currency"]] += payment 2558 2559 else: 2560 customStat["coupons"][item["payment"]["currency"]] = payment 2561 2562 # count broker commissions: 2563 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2564 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2565 customStat["brokerCom"][item["payment"]["currency"]] += payment 2566 2567 else: 2568 customStat["brokerCom"][item["payment"]["currency"]] = payment 2569 2570 # count service commissions: 2571 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2572 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2573 customStat["serviceCom"][item["payment"]["currency"]] += payment 2574 2575 else: 2576 customStat["serviceCom"][item["payment"]["currency"]] = payment 2577 2578 # count margin commissions: 2579 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2580 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2581 customStat["marginCom"][item["payment"]["currency"]] += payment 2582 2583 else: 2584 customStat["marginCom"][item["payment"]["currency"]] = payment 2585 2586 # count withholding taxes: 2587 elif "_TAX" in item["operationType"]: 2588 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2589 customStat["allTaxes"][item["payment"]["currency"]] += payment 2590 2591 else: 2592 customStat["allTaxes"][item["payment"]["currency"]] = payment 2593 2594 else: 2595 continue 2596 2597 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2598 2599 # --- view "Actions" lines: 2600 info.extend([ 2601 "| Report sections | | | | |\n", 2602 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2603 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2604 "| | Buy: {:<22} | {:<28} | | |\n".format( 2605 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2606 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2607 ), 2608 "| | Sell: {:<21} | {:<28} | | |\n".format( 2609 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2610 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2611 ), 2612 ]) 2613 2614 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2615 for key in opsKeys: 2616 if key == "rub": 2617 continue 2618 2619 info.extend([ 2620 "| | | {:<28} | | |\n".format( 2621 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2622 ), 2623 "| | | {:<28} | | |\n".format( 2624 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2625 ), 2626 ]) 2627 2628 info.append(splitLine1) 2629 2630 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2631 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2632 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2633 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2634 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2635 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2636 ) 2637 2638 # --- view "Payments" lines: 2639 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2640 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2641 2642 for key in paymentsKeys: 2643 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2644 2645 info.append(splitLine1) 2646 2647 # --- view "Commissions and taxes" lines: 2648 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2649 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2650 2651 for key in comKeys: 2652 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2653 2654 info.extend([ 2655 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2656 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2657 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2658 ]) 2659 2660 else: 2661 info.append("Broker returned no operations during this period\n") 2662 2663 # --- view "Operations" section: 2664 for item in ops: 2665 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2666 continue 2667 2668 else: 2669 self._figi = item["figi"] 2670 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2671 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2672 2673 # group of deals during one day: 2674 if nextDay and item["date"].split("T")[0] != nextDay: 2675 info.append(splitLine2) 2676 nextDay = "" 2677 2678 else: 2679 nextDay = item["date"].split("T")[0] # saving current day for splitting 2680 2681 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2682 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2683 self._figi if self._figi else "—", 2684 instrument["ticker"] if instrument else "—", 2685 instrument["type"] if instrument else "—", 2686 item["quantity"] if int(item["quantity"]) > 0 else "—", 2687 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2688 TKS_OPERATION_STATES[item["state"]], 2689 TKS_OPERATION_TYPES[item["operationType"]], 2690 )) 2691 2692 infoText = "".join(info) 2693 2694 if show and not onlyFiles: 2695 if self.moreDebug: 2696 uLogger.debug("Records about history of a client's operations successfully received") 2697 2698 uLogger.info(infoText) 2699 2700 if self.reportFile and (show or onlyFiles): 2701 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2702 fH.write(infoText) 2703 2704 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2705 2706 if self.useHTMLReports: 2707 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2708 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2709 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2710 2711 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2712 2713 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2715 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2716 """ 2717 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2718 2719 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2720 Warning! Broker server used ISO UTC time by default. 2721 2722 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2723 Also, `historyFile` used to update history with `onlyMissing` parameter. 2724 2725 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2726 2727 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2728 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2729 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2730 `"hour"`, `"day"`. Default: `"hour"`. 2731 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2732 False by default. Warning! History appends only from last candle to current time 2733 with always update last candle! 2734 :param csvSep: separator if csv-file is used, `,` by default. 2735 :param show: if `True` then also prints Pandas DataFrame to the console. 2736 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2737 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2738 `["date", "time", "open", "high", "low", "close", "volume"]`. 2739 """ 2740 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2741 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2742 history = None # empty pandas object for history 2743 2744 if interval not in TKS_CANDLE_INTERVALS.keys(): 2745 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2746 raise Exception("Incorrect value") 2747 2748 if not (self._ticker or self._figi): 2749 uLogger.error("Ticker or FIGI must be defined!") 2750 raise Exception("Ticker or FIGI required") 2751 2752 if self._ticker and not self._figi: 2753 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2754 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2755 2756 if self._figi and not self._ticker: 2757 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2758 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2759 2760 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2761 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2762 if interval.lower() != "day": 2763 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2764 2765 delta = dtEnd - dtStart # current UTC time minus last time in file 2766 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2767 2768 # calculate history length in candles: 2769 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2770 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2771 length += 1 # to avoid fraction time 2772 2773 # calculate data blocks count: 2774 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2775 2776 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2777 if self.moreDebug: 2778 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2779 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2780 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2781 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2782 2783 tempOld = None # pandas object for old history, if --only-missing key present 2784 lastTime = None # datetime object of last old candle in file 2785 2786 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2787 if self.moreDebug: 2788 uLogger.debug("--only-missing key present, add only last missing candles...") 2789 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2790 2791 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2792 2793 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2794 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2795 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2796 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2797 2798 # get last datetime object from last string in file or minus 1 delta if file is empty: 2799 if len(tempOld) > 0: 2800 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2801 2802 else: 2803 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2804 2805 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2806 2807 responseJSONs = [] # raw history blocks of data 2808 2809 blockEnd = dtEnd 2810 for item in range(blocks): 2811 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2812 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2813 2814 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2815 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2816 )) 2817 2818 if blockStart == blockEnd: 2819 uLogger.debug("Skipped this zero-length block...") 2820 2821 else: 2822 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2823 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2824 self.body = str({ 2825 "figi": self._figi, 2826 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2827 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2828 "interval": TKS_CANDLE_INTERVALS[interval][0] 2829 }) 2830 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2831 2832 if "code" in responseJSON.keys(): 2833 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2834 2835 else: 2836 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2837 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2838 2839 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2840 2841 blockEnd = blockStart 2842 2843 printCount = len(responseJSONs) # candles to show in console 2844 if responseJSONs: 2845 tempHistory = pd.DataFrame( 2846 data={ 2847 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2848 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2849 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2850 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2851 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2852 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2853 "volume": [int(item["volume"]) for item in responseJSONs], 2854 }, 2855 index=range(len(responseJSONs)), 2856 columns=["date", "time", "open", "high", "low", "close", "volume"], 2857 ) 2858 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2859 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2860 2861 # append only newest candles to old history if --only-missing key present: 2862 if onlyMissing and tempOld is not None and lastTime is not None: 2863 index = 0 # find start index in tempHistory data: 2864 2865 for i, item in tempHistory.iterrows(): 2866 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2867 2868 if curTime == lastTime: 2869 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2870 index = i 2871 printCount = i + 1 2872 break 2873 2874 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2875 2876 else: 2877 history = tempHistory # if no `--only-missing` key then load full data from server 2878 2879 if self.moreDebug: 2880 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2881 2882 if history is not None and not history.empty: 2883 if show and not onlyFiles: 2884 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2885 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2886 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2887 )) 2888 2889 else: 2890 uLogger.warning("Received an empty candles history!") 2891 2892 if self.historyFile is not None: 2893 if history is not None and not history.empty: 2894 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2895 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2896 2897 else: 2898 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2899 2900 else: 2901 if self.moreDebug: 2902 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2903 2904 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2906 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2907 """ 2908 Load candles history from csv-file and return Pandas DataFrame object. 2909 2910 See also: `History()` and `ShowHistoryChart()` methods. 2911 2912 :param filePath: path to csv-file to open. 2913 """ 2914 loadedHistory = None # init candles data object 2915 2916 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2917 2918 if os.path.exists(filePath): 2919 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2920 2921 tfStr = self.priceModel.FormattedDelta( 2922 self.priceModel.timeframe, 2923 "{days} days {hours}h {minutes}m {seconds}s", 2924 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2925 self.priceModel.timeframe, 2926 "{hours}h {minutes}m {seconds}s", 2927 ) 2928 2929 if loadedHistory is not None and not loadedHistory.empty: 2930 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2931 len(loadedHistory), 2932 tfStr, 2933 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2934 ) 2935 2936 else: 2937 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2938 2939 else: 2940 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2941 2942 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2944 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2945 """ 2946 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2947 2948 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2949 Default: `index.html` (both for interact and non-interact candlesticks chart). 2950 2951 See also: `History()` and `LoadHistory()` methods. 2952 2953 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2954 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2955 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2956 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2957 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2958 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2959 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2960 """ 2961 if isinstance(candles, str): 2962 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2963 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2964 2965 elif isinstance(candles, pd.DataFrame): 2966 self.priceModel.prices = candles # set candles chain from variable 2967 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2968 2969 if "datetime" not in candles.columns: 2970 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2971 2972 else: 2973 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2974 raise Exception("Incorrect value") 2975 2976 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2977 2978 if interact: 2979 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2980 2981 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2982 2983 else: 2984 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2985 2986 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2987 2988 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2990 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2991 """ 2992 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2993 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2994 2995 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2996 2997 :param operation: string "Buy" or "Sell". 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 3000 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 3001 :param expDate: string "Undefined" by default or local date in future, 3002 it is a string with format `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 if self.accountId is None or not self.accountId: 3006 uLogger.error("Variable `accountId` must be defined for using this method!") 3007 raise Exception("Account ID required") 3008 3009 if operation is None or not operation or operation not in ("Buy", "Sell"): 3010 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3011 raise Exception("Incorrect value") 3012 3013 if lots is None or lots < 1: 3014 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3015 lots = 1 3016 3017 if tp is None or tp < 0: 3018 tp = 0 3019 3020 if sl is None or sl < 0: 3021 sl = 0 3022 3023 if expDate is None or not expDate: 3024 expDate = "Undefined" 3025 3026 if not (self._ticker or self._figi): 3027 uLogger.error("Ticker or FIGI must be defined!") 3028 raise Exception("Ticker or FIGI required") 3029 3030 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3031 self._ticker = instrument["ticker"] 3032 self._figi = instrument["figi"] 3033 3034 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3035 3036 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3037 self.body = str({ 3038 "figi": self._figi, 3039 "quantity": str(lots), 3040 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3041 "accountId": str(self.accountId), 3042 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3043 }) 3044 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3045 3046 if "orderId" in response.keys(): 3047 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3048 operation, response["orderId"], 3049 self._ticker, self._figi, lots, 3050 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3051 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3052 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3053 )) 3054 3055 if tp > 0: 3056 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3057 3058 if sl > 0: 3059 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3060 3061 else: 3062 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3063 3064 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3066 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3067 """ 3068 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3069 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3070 3071 See also: `Order()` and `Trade()` docstrings. 3072 3073 :param lots: volume, integer count of lots >= 1. 3074 :param tp: float > 0, take profit price of stop-order. 3075 :param sl: float > 0, stop loss price of stop-order. 3076 :param expDate: it's a local date in future. 3077 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3078 :return: JSON with response from broker server. 3079 """ 3080 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3082 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3083 """ 3084 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3085 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3086 3087 See also: `Order()` and `Trade()` docstrings. 3088 3089 :param lots: volume, integer count of lots >= 1. 3090 :param tp: float > 0, take profit price of stop-order. 3091 :param sl: float > 0, stop loss price of stop-order. 3092 :param expDate: it's a local date in the future. 3093 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3094 :return: JSON with response from broker server. 3095 """ 3096 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3098 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3099 """ 3100 Close position of given instruments. 3101 3102 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3103 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3104 This avoids unnecessary downloading data from the server. 3105 """ 3106 if instruments is None or not instruments: 3107 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3108 raise Exception("Ticker or FIGI required") 3109 3110 if isinstance(instruments, str): 3111 instruments = [instruments] 3112 3113 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3114 if uniqueInstruments: 3115 if portfolio is None or not portfolio: 3116 portfolio = self.Overview(show=False) 3117 3118 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3119 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3120 3121 for self._figi in uniqueInstruments: 3122 if self._figi not in allOpened: 3123 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3124 continue 3125 3126 # search open trade info about instrument by ticker: 3127 instrument = {} 3128 for iType in TKS_INSTRUMENTS: 3129 if instrument: 3130 break 3131 3132 for item in portfolio["stat"][iType]: 3133 if item["figi"] == self._figi: 3134 instrument = item 3135 break 3136 3137 if instrument: 3138 self._ticker = instrument["ticker"] 3139 self._figi = instrument["figi"] 3140 3141 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3142 self._ticker, 3143 self._figi, 3144 int(instrument["volume"]), 3145 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3146 )) 3147 3148 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3149 3150 if tradeLots > 0: 3151 if instrument["blocked"] > 0: 3152 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3153 instrument["blocked"], 3154 self._ticker, 3155 tradeLots, 3156 )) 3157 3158 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3159 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3160 3161 else: 3162 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3164 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3165 """ 3166 Close all positions of given instruments with defined type. 3167 3168 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3169 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3170 This avoids unnecessary downloading data from the server. 3171 """ 3172 if iType not in TKS_INSTRUMENTS: 3173 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3174 3175 else: 3176 if portfolio is None or not portfolio: 3177 portfolio = self.Overview(show=False) 3178 3179 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3180 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3181 3182 if tickers and portfolio: 3183 self.CloseTrades(tickers, portfolio) 3184 3185 else: 3186 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3188 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3189 """ 3190 Universal method to create market or limit orders with all available parameters for current `accountId`. 3191 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3192 3193 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3194 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3195 3196 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3197 then broker immediately open market order as you can do simple --buy or --sell operations! 3198 3199 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3200 When current price will go up or down to target price value then broker opens a limit order. 3201 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3202 3203 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3204 3205 :param operation: string "Buy" or "Sell". 3206 :param orderType: string "Limit" or "Stop". 3207 :param lots: volume, integer count of lots >= 1. 3208 :param targetPrice: target price > 0. This is open trade price for limit order. 3209 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3210 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3211 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3212 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3213 Stop loss order always executed by market price. 3214 :param expDate: string "Undefined" by default or local date in future. 3215 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3216 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3217 A limit order has no expiration date, it lasts until the end of the trading day. 3218 :return: JSON with response from broker server. 3219 """ 3220 if self.accountId is None or not self.accountId: 3221 uLogger.error("Variable `accountId` must be defined for using this method!") 3222 raise Exception("Account ID required") 3223 3224 if operation is None or not operation or operation not in ("Buy", "Sell"): 3225 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3226 raise Exception("Incorrect value") 3227 3228 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3229 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3230 raise Exception("Incorrect value") 3231 3232 if lots is None or lots < 1: 3233 uLogger.error("You must define trade volume > 0: integer count of lots!") 3234 raise Exception("Incorrect value") 3235 3236 if targetPrice is None or targetPrice <= 0: 3237 uLogger.error("Target price for limit-order must be greater than 0!") 3238 raise Exception("Incorrect value") 3239 3240 if limitPrice is None or limitPrice <= 0: 3241 limitPrice = targetPrice 3242 3243 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3244 stopType = "Limit" 3245 3246 if expDate is None or not expDate: 3247 expDate = "Undefined" 3248 3249 if not (self._ticker or self._figi): 3250 uLogger.error("Tocker or FIGI must be defined!") 3251 raise Exception("Ticker or FIGI required") 3252 3253 response = {} 3254 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3255 self._ticker = instrument["ticker"] 3256 self._figi = instrument["figi"] 3257 3258 if orderType == "Limit": 3259 uLogger.debug( 3260 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3261 self._ticker, self._figi, 3262 operation, lots, targetPrice, instrument["currency"], 3263 )) 3264 3265 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3266 self.body = str({ 3267 "figi": self._figi, 3268 "quantity": str(lots), 3269 "price": FloatToNano(targetPrice), 3270 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3271 "accountId": str(self.accountId), 3272 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3273 }) 3274 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3275 3276 if "orderId" in response.keys(): 3277 uLogger.info( 3278 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3279 response["orderId"], self._ticker, self._figi, operation, lots, 3280 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3281 )) 3282 3283 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3284 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3285 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3286 targetPrice, instrument["currency"], 3287 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3288 )) 3289 3290 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3291 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3292 targetPrice, instrument["currency"], 3293 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3294 )) 3295 3296 else: 3297 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3298 3299 if orderType == "Stop": 3300 uLogger.debug( 3301 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3302 self._ticker, self._figi, 3303 operation, lots, 3304 targetPrice, instrument["currency"], 3305 limitPrice, instrument["currency"], 3306 stopType, expDate, 3307 )) 3308 3309 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3310 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3311 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3312 3313 body = { 3314 "figi": self._figi, 3315 "quantity": str(lots), 3316 "price": FloatToNano(limitPrice), 3317 "stopPrice": FloatToNano(targetPrice), 3318 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3319 "accountId": str(self.accountId), 3320 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3321 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3322 } 3323 3324 if expDateUTC: 3325 body["expireDate"] = expDateUTC 3326 3327 self.body = str(body) 3328 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3329 3330 if "stopOrderId" in response.keys(): 3331 uLogger.info( 3332 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3333 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3334 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3335 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3336 TKS_STOP_ORDER_TYPES[stopOrderType], 3337 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3338 )) 3339 3340 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3341 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3342 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{} {}] is lower than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3343 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3344 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3345 )) 3346 3347 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3348 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{} {}] is higher than the current price [{} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3349 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3350 "{:.4f}".format(instrument["currentPrice"]["lastPrice"]).rstrip("0").rstrip("."), instrument["currency"], 3351 )) 3352 3353 else: 3354 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3355 3356 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3358 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3359 """ 3360 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3361 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3362 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3363 See also: `Order()` docstring. 3364 3365 :param lots: volume, integer count of lots >= 1. 3366 :param targetPrice: target price > 0. This is open trade price for limit order. 3367 :return: JSON with response from broker server. 3368 """ 3369 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3371 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3372 """ 3373 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3374 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3375 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3376 target price value then broker opens a limit order. See also: `Order()` docstring. 3377 3378 :param lots: volume, integer count of lots >= 1. 3379 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3380 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3381 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3382 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3383 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3384 :param expDate: string "Undefined" by default or local date in future. 3385 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3386 This date is converting to UTC format for server. 3387 :return: JSON with response from broker server. 3388 """ 3389 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3391 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3392 """ 3393 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3394 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3395 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3396 See also: `Order()` docstring. 3397 3398 :param lots: volume, integer count of lots >= 1. 3399 :param targetPrice: target price > 0. This is open trade price for limit order. 3400 :return: JSON with response from broker server. 3401 """ 3402 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3404 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3405 """ 3406 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3407 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3408 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3409 target price value then broker opens a limit order. See also: `Order()` docstring. 3410 3411 :param lots: volume, integer count of lots >= 1. 3412 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3413 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3414 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3415 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3416 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3417 :param expDate: string "Undefined" by default or local date in future. 3418 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3419 This date is converting to UTC format for server. 3420 :return: JSON with response from broker server. 3421 """ 3422 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3424 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3425 """ 3426 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3427 3428 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3429 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3430 This avoids unnecessary downloading data from the server. 3431 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3432 """ 3433 if self.accountId is None or not self.accountId: 3434 uLogger.error("Variable `accountId` must be defined for using this method!") 3435 raise Exception("Account ID required") 3436 3437 if orderIDs: 3438 if allOrdersIDs is None: 3439 rawOrders = self.RequestPendingOrders() 3440 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3441 3442 if allStopOrdersIDs is None: 3443 rawStopOrders = self.RequestStopOrders() 3444 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3445 3446 for orderID in orderIDs: 3447 idInPendingOrders = orderID in allOrdersIDs 3448 idInStopOrders = orderID in allStopOrdersIDs 3449 3450 if not (idInPendingOrders or idInStopOrders): 3451 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3452 continue 3453 3454 else: 3455 if idInPendingOrders: 3456 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3457 3458 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3459 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3460 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3461 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3462 3463 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3464 if self.moreDebug: 3465 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3466 3467 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3468 3469 else: 3470 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3471 3472 elif idInStopOrders: 3473 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3474 3475 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3476 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3477 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3478 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3479 3480 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3481 if self.moreDebug: 3482 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3483 3484 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3485 3486 else: 3487 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3488 3489 else: 3490 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3492 def CloseAllOrders(self) -> None: 3493 """ 3494 Gets a list of open pending and stop orders and cancel it all. 3495 """ 3496 rawOrders = self.RequestPendingOrders() 3497 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3498 lenOrders = len(allOrdersIDs) 3499 3500 rawStopOrders = self.RequestStopOrders() 3501 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3502 lenSOrders = len(allStopOrdersIDs) 3503 3504 if lenOrders > 0 or lenSOrders > 0: 3505 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3506 3507 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3508 3509 else: 3510 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3512 def CloseAll(self, *args) -> None: 3513 """ 3514 Close all available (not blocked) opened trades and orders. 3515 3516 Also, you can select one or more keywords case-insensitive: 3517 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3518 3519 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3520 """ 3521 overview = self.Overview(show=False) # get all open trades info 3522 3523 if len(args) == 0: 3524 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3525 self.CloseAllOrders() # close all pending and stop orders 3526 3527 for iType in TKS_INSTRUMENTS: 3528 if iType != "Currencies": 3529 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3530 3531 else: 3532 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3533 lowerArgs = [x.lower() for x in args] 3534 3535 if "orders" in lowerArgs: 3536 self.CloseAllOrders() # close all pending and stop orders 3537 3538 for iType in TKS_INSTRUMENTS: 3539 if iType.lower() in lowerArgs and iType != "Currencies": 3540 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3542 def CloseAllByTicker(self, instrument: str) -> None: 3543 """ 3544 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3545 3546 This method searches opened trade and orders of instrument throw all portfolio and then use 3547 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3548 3549 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3550 3551 :param instrument: string with ticker. 3552 """ 3553 if instrument is None or not instrument: 3554 uLogger.error("Ticker name must be defined for using this method!") 3555 raise Exception("Ticker required") 3556 3557 overview = self.Overview(show=False) # get user portfolio with all open trades info 3558 3559 self._ticker = instrument # try to set instrument as ticker 3560 self._figi = "" 3561 3562 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3563 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3564 3565 if limitAll and self.IsInLimitOrders(portfolio=overview): 3566 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3567 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3568 3569 if stopAll and self.IsInStopOrders(portfolio=overview): 3570 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3571 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3572 3573 if self.IsInPortfolio(portfolio=overview): 3574 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3575 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3577 def CloseAllByFIGI(self, instrument: str) -> None: 3578 """ 3579 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3580 3581 This method searches opened trade and orders of instrument throw all portfolio and then use 3582 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3583 3584 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3585 3586 :param instrument: string with FIGI id. 3587 """ 3588 if instrument is None or not instrument: 3589 uLogger.error("FIGI id must be defined for using this method!") 3590 raise Exception("FIGI required") 3591 3592 overview = self.Overview(show=False) # get user portfolio with all open trades info 3593 3594 self._ticker = "" 3595 self._figi = instrument # try to set instrument as FIGI id 3596 3597 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3598 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3599 3600 if limitAll and self.IsInLimitOrders(portfolio=overview): 3601 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3602 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3603 3604 if stopAll and self.IsInStopOrders(portfolio=overview): 3605 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3606 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3607 3608 if self.IsInPortfolio(portfolio=overview): 3609 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3610 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3612 @staticmethod 3613 def ParseOrderParameters(operation, **inputParameters): 3614 """ 3615 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3616 3617 :param operation: string "Buy" or "Sell". 3618 :param inputParameters: this is dict of strings that looks like this 3619 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3620 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3621 "prices" key: one or more prices to open limit-orders 3622 Counts of values in lots and prices lists must be equals! 3623 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3624 """ 3625 # TODO: update order grid work with api v2 3626 pass 3627 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3628 # 3629 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3630 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3631 # raise Exception("Incorrect value") 3632 # 3633 # if "l" in inputParameters.keys(): 3634 # inputParameters["lots"] = inputParameters.pop("l") 3635 # 3636 # if "p" in inputParameters.keys(): 3637 # inputParameters["prices"] = inputParameters.pop("p") 3638 # 3639 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3640 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3641 # raise Exception("Incorrect value") 3642 # 3643 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3644 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3645 # 3646 # if len(lots) != len(prices): 3647 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3648 # raise Exception("Incorrect value") 3649 # 3650 # uLogger.debug("Extracted parameters for orders:") 3651 # uLogger.debug("lots = {}".format(lots)) 3652 # uLogger.debug("prices = {}".format(prices)) 3653 # 3654 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3655 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3656 # uLogger.debug("Order parameters: {}".format(result)) 3657 # 3658 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3660 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3661 """ 3662 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3663 3664 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3665 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3666 """ 3667 result = False 3668 msg = "Instrument not defined!" 3669 3670 if portfolio is None or not portfolio: 3671 portfolio = self.Overview(show=False) 3672 3673 if self._ticker: 3674 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3675 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3676 3677 for iType in TKS_INSTRUMENTS: 3678 for instrument in portfolio["stat"][iType]: 3679 if instrument["ticker"] == self._ticker: 3680 result = True 3681 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3682 break 3683 3684 elif self._figi: 3685 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3686 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3687 3688 for iType in TKS_INSTRUMENTS: 3689 for instrument in portfolio["stat"][iType]: 3690 if instrument["figi"] == self._figi: 3691 result = True 3692 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3693 break 3694 3695 else: 3696 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3697 3698 uLogger.debug(msg) 3699 3700 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3702 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3703 """ 3704 Returns instrument from the user's portfolio if it presents there. 3705 Instrument must be defined by `ticker` (highly priority) or `figi`. 3706 3707 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3708 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3709 """ 3710 result = None 3711 msg = "Instrument not defined!" 3712 3713 if portfolio is None or not portfolio: 3714 portfolio = self.Overview(show=False) 3715 3716 if self._ticker: 3717 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3718 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3719 3720 for iType in TKS_INSTRUMENTS: 3721 for instrument in portfolio["stat"][iType]: 3722 if instrument["ticker"] == self._ticker: 3723 result = instrument 3724 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3725 break 3726 3727 elif self._figi: 3728 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3729 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3730 3731 for iType in TKS_INSTRUMENTS: 3732 for instrument in portfolio["stat"][iType]: 3733 if instrument["figi"] == self._figi: 3734 result = instrument 3735 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3736 break 3737 3738 else: 3739 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3740 3741 uLogger.debug(msg) 3742 3743 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3745 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3746 """ 3747 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3748 3749 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3750 3751 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3752 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3753 """ 3754 result = False 3755 msg = "Instrument not defined!" 3756 3757 if portfolio is None or not portfolio: 3758 portfolio = self.Overview(show=False) 3759 3760 if self._ticker: 3761 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3762 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3763 3764 for instrument in portfolio["stat"]["orders"]: 3765 if instrument["ticker"] == self._ticker: 3766 result = True 3767 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3768 break 3769 3770 elif self._figi: 3771 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3772 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3773 3774 for instrument in portfolio["stat"]["orders"]: 3775 if instrument["figi"] == self._figi: 3776 result = True 3777 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3778 break 3779 3780 else: 3781 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3782 3783 uLogger.debug(msg) 3784 3785 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3787 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3788 """ 3789 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3790 Instrument must be defined by `ticker` (highly priority) or `figi`. 3791 3792 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3793 3794 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3795 :return: list with `orderID`s of limit orders. 3796 """ 3797 result = [] 3798 msg = "Instrument not defined!" 3799 3800 if portfolio is None or not portfolio: 3801 portfolio = self.Overview(show=False) 3802 3803 if self._ticker: 3804 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3805 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3806 3807 for instrument in portfolio["stat"]["orders"]: 3808 if instrument["ticker"] == self._ticker: 3809 result.append(instrument["orderID"]) 3810 3811 if result: 3812 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3813 3814 elif self._figi: 3815 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3816 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3817 3818 for instrument in portfolio["stat"]["orders"]: 3819 if instrument["figi"] == self._figi: 3820 result.append(instrument["orderID"]) 3821 3822 if result: 3823 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3824 3825 else: 3826 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3827 3828 uLogger.debug(msg) 3829 3830 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3832 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3833 """ 3834 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3835 3836 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3837 3838 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3839 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3840 """ 3841 result = False 3842 msg = "Instrument not defined!" 3843 3844 if portfolio is None or not portfolio: 3845 portfolio = self.Overview(show=False) 3846 3847 if self._ticker: 3848 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3849 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3850 3851 for instrument in portfolio["stat"]["stopOrders"]: 3852 if instrument["ticker"] == self._ticker: 3853 result = True 3854 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3855 break 3856 3857 elif self._figi: 3858 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3859 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3860 3861 for instrument in portfolio["stat"]["stopOrders"]: 3862 if instrument["figi"] == self._figi: 3863 result = True 3864 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3865 break 3866 3867 else: 3868 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3869 3870 uLogger.debug(msg) 3871 3872 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3874 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3875 """ 3876 Returns list with all `orderID`s of opened stop orders for the instrument. 3877 Instrument must be defined by `ticker` (highly priority) or `figi`. 3878 3879 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3880 3881 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3882 :return: list with `orderID`s of stop orders. 3883 """ 3884 result = [] 3885 msg = "Instrument not defined!" 3886 3887 if portfolio is None or not portfolio: 3888 portfolio = self.Overview(show=False) 3889 3890 if self._ticker: 3891 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3892 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3893 3894 for instrument in portfolio["stat"]["stopOrders"]: 3895 if instrument["ticker"] == self._ticker: 3896 result.append(instrument["orderID"]) 3897 3898 if result: 3899 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3900 3901 elif self._figi: 3902 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3903 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3904 3905 for instrument in portfolio["stat"]["stopOrders"]: 3906 if instrument["figi"] == self._figi: 3907 result.append(instrument["orderID"]) 3908 3909 if result: 3910 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3911 3912 else: 3913 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3914 3915 uLogger.debug(msg) 3916 3917 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3919 def RequestLimits(self) -> dict: 3920 """ 3921 Method for obtaining the available funds for withdrawal for current `accountId`. 3922 3923 See also: 3924 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3925 - `OverviewLimits()` method 3926 3927 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3928 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3929 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3930 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3931 """ 3932 if self.accountId is None or not self.accountId: 3933 uLogger.error("Variable `accountId` must be defined for using this method!") 3934 raise Exception("Account ID required") 3935 3936 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3937 3938 self.body = str({"accountId": self.accountId}) 3939 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3940 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3941 3942 if self.moreDebug: 3943 uLogger.debug("Records about available funds for withdrawal successfully received") 3944 3945 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3947 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3948 """ 3949 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3950 3951 See also: `RequestLimits()`. 3952 3953 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3954 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3955 :return: dict with raw parsed data from server and some calculated statistics about it. 3956 """ 3957 if self.accountId is None or not self.accountId: 3958 uLogger.error("Variable `accountId` must be defined for using this method!") 3959 raise Exception("Account ID required") 3960 3961 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3962 3963 view = { 3964 "rawLimits": rawLimits, 3965 "limits": { # parsed data for every currency: 3966 "money": { # this is an array of portfolio currency positions 3967 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3968 }, 3969 "blocked": { # this is an array of blocked currency 3970 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3971 }, 3972 "blockedGuarantee": { # this is locked money under collateral for futures 3973 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3974 }, 3975 }, 3976 } 3977 3978 # --- Prepare text table with limits in human-readable format: 3979 if show or onlyFiles: 3980 info = [ 3981 "# Withdrawal limits\n\n", 3982 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3983 "* **Account ID:** [{}]\n".format(self.accountId), 3984 ] 3985 3986 if view["limits"]["money"]: 3987 info.extend([ 3988 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3989 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3990 ]) 3991 3992 else: 3993 info.append("\nNo withdrawal limits\n") 3994 3995 for curr in view["limits"]["money"].keys(): 3996 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3997 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3998 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3999 4000 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 4001 "[{}]".format(curr), 4002 "{:.2f}".format(view["limits"]["money"][curr]), 4003 "{:.2f}".format(availableMoney), 4004 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4005 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4006 ) 4007 4008 if curr == "rub": 4009 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4010 4011 else: 4012 info.append(infoStr) 4013 4014 infoText = "".join(info) 4015 4016 if show and not onlyFiles: 4017 uLogger.info(infoText) 4018 4019 if self.withdrawalLimitsFile and (show or onlyFiles): 4020 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4021 fH.write(infoText) 4022 4023 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4024 4025 if self.useHTMLReports: 4026 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4027 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4028 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4029 4030 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4031 4032 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4034 def RequestAccounts(self) -> dict: 4035 """ 4036 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4037 4038 See also: 4039 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4040 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4041 - `OverviewUserInfo()` method 4042 4043 :return: dict with raw data from server that contains accounts info. Example of dict: 4044 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4045 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4046 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4047 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4048 """ 4049 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4050 4051 self.body = str({}) 4052 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4053 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4054 4055 if self.moreDebug: 4056 uLogger.debug("Records about available accounts successfully received") 4057 4058 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4060 def RequestUserInfo(self) -> dict: 4061 """ 4062 Method for requesting common user's information. 4063 4064 See also: 4065 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4066 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4067 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4068 - `OverviewUserInfo()` method 4069 4070 :return: dict with raw data from server that contains user's information. Example of dict: 4071 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4072 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4073 """ 4074 uLogger.debug("Requesting common user's information. Wait, please...") 4075 4076 self.body = str({}) 4077 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4078 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4079 4080 if self.moreDebug: 4081 uLogger.debug("Records about current user successfully received") 4082 4083 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4085 def RequestMarginStatus(self, accountId: str = None) -> dict: 4086 """ 4087 Method for requesting margin calculation for defined account ID. 4088 4089 See also: 4090 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4091 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4092 - `OverviewUserInfo()` method 4093 4094 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4095 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4096 Example of responses: 4097 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4098 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4099 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4100 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4101 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4102 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4103 """ 4104 if accountId is None or not accountId: 4105 if self.accountId is None or not self.accountId: 4106 uLogger.error("Variable `accountId` must be defined for using this method!") 4107 raise Exception("Account ID required") 4108 4109 else: 4110 accountId = self.accountId # use `self.accountId` (main ID) by default 4111 4112 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4113 4114 self.body = str({"accountId": accountId}) 4115 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4116 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4117 4118 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4119 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4120 rawMargin = {} 4121 4122 else: 4123 if self.moreDebug: 4124 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4125 4126 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4128 def RequestTariffLimits(self) -> dict: 4129 """ 4130 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4131 4132 See also: 4133 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4134 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4135 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4136 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4137 - `OverviewUserInfo()` method 4138 4139 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4140 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4141 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4142 """ 4143 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4144 4145 self.body = str({}) 4146 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4147 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4148 4149 if self.moreDebug: 4150 uLogger.debug("Records with limits of current tariff successfully received") 4151 4152 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4154 def RequestBondCoupons(self, iJSON: dict) -> dict: 4155 """ 4156 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4157 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4158 All dates are in UTC timezone. 4159 4160 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4161 Documentation: 4162 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4163 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4164 4165 See also: `ExtendBondsData()`. 4166 4167 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4168 If raw iJSON is not data of bond then server returns an error [400] with message: 4169 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4170 :return: dictionary with bond payment calendar. Response example 4171 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4172 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4173 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4174 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4175 """ 4176 if iJSON["figi"] is None or not iJSON["figi"]: 4177 uLogger.error("FIGI must be defined for using this method!") 4178 raise Exception("FIGI required") 4179 4180 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4181 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4182 4183 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4184 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4185 self._figi, 4186 startDate, 4187 endDate, 4188 )) 4189 4190 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4191 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4192 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4193 4194 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4195 uLogger.warning("Instrument type is not bond!") 4196 4197 else: 4198 if self.moreDebug: 4199 uLogger.debug("Records about bond payment calendar successfully received") 4200 4201 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4203 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4204 """ 4205 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4206 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4207 coupon yields, current yields and some statistics etc. 4208 4209 WARNING! This is too long operation if a lot of bonds requested from broker server. 4210 4211 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4212 4213 :param instruments: list of strings with tickers or FIGIs. 4214 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4215 for further used by data scientists or stock analytics. 4216 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4217 In XLSX-file and Pandas DataFrame fields mean: 4218 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4219 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4220 """ 4221 if instruments is None or not instruments: 4222 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4223 raise Exception("Ticker or FIGI required") 4224 4225 if isinstance(instruments, str): 4226 instruments = [instruments] 4227 4228 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4229 4230 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4231 4232 iCount = len(uniqueInstruments) 4233 tooLong = iCount >= 20 4234 if tooLong: 4235 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4236 4237 bonds = None 4238 for i, self._figi in enumerate(uniqueInstruments): 4239 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4240 4241 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4242 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4243 rawBond = self.SearchByFIGI(requestPrice=True) 4244 4245 # Widen raw data with UTC current time (iData["actualDateTime"]): 4246 actualDate = datetime.now(tzutc()) 4247 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4248 4249 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4250 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4251 4252 # Replace some values with human-readable: 4253 iData["nominalCurrency"] = iData["nominal"]["currency"] 4254 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4255 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4256 iData["aciCurrency"] = iData["aciValue"]["currency"] 4257 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4258 iData["issueSize"] = int(iData["issueSize"]) 4259 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4260 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4261 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4262 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4263 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4264 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4265 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4266 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4267 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4268 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4269 4270 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4271 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4272 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4273 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4274 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4275 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4276 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4277 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4278 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4279 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4280 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4281 4282 # Widen raw data with calendar data from `rawCalendar` values: 4283 calendarData = [] 4284 if "events" in iData["rawCalendar"].keys(): 4285 for item in iData["rawCalendar"]["events"]: 4286 calendarData.append({ 4287 "couponDate": item["couponDate"], 4288 "couponNumber": int(item["couponNumber"]), 4289 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4290 "payCurrency": item["payOneBond"]["currency"], 4291 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4292 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4293 "couponStartDate": item["couponStartDate"], 4294 "couponEndDate": item["couponEndDate"], 4295 "couponPeriod": item["couponPeriod"], 4296 }) 4297 4298 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4299 if "maturityDate" not in iData.keys(): 4300 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4301 4302 # Widen raw data with Coupon Rate. 4303 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4304 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4305 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4306 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4307 4308 # Widen raw data with Yield to Maturity (YTM) on current date. 4309 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4310 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4311 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4312 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4313 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4314 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4315 4316 iData["calendar"] = calendarData # adds calendar at the end 4317 4318 # Remove not used data: 4319 iData.pop("uid") 4320 iData.pop("positionUid") 4321 iData.pop("currentPrice") 4322 iData.pop("rawCalendar") 4323 4324 colNames = list(iData.keys()) 4325 if bonds is None: 4326 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4327 4328 else: 4329 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4330 4331 else: 4332 uLogger.warning("Instrument is not a bond!") 4333 4334 processed = round(100 * (i + 1) / iCount, 1) 4335 if tooLong and processed % 5 == 0: 4336 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4337 4338 else: 4339 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4340 4341 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4342 4343 # Saving bonds from Pandas DataFrame to XLSX sheet: 4344 if xlsx and self.bondsXLSXFile: 4345 with pd.ExcelWriter( 4346 path=self.bondsXLSXFile, 4347 date_format=TKS_DATE_FORMAT, 4348 datetime_format=TKS_DATE_TIME_FORMAT, 4349 mode="w", 4350 ) as writer: 4351 bonds.to_excel( 4352 writer, 4353 sheet_name="Extended bonds data", 4354 index=True, 4355 encoding="UTF-8", 4356 freeze_panes=(1, 1), 4357 ) # saving as XLSX-file with freeze first row and column as headers 4358 4359 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4360 4361 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4363 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4364 """ 4365 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4366 4367 WARNING! This is too long operation if a lot of bonds requested from broker server. 4368 4369 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4370 4371 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4372 extended information about bonds: main info, current prices, bond payment calendar, 4373 coupon yields, current yields and some statistics etc. 4374 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4375 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4376 for further used by data scientists or stock analytics. 4377 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4378 """ 4379 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4380 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4381 4382 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4383 4384 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4385 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4386 calendar = None 4387 for bond in extBonds.iterrows(): 4388 for item in bond[1]["calendar"]: 4389 cData = { 4390 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4391 "couponDate": item["couponDate"], 4392 "figi": bond[1]["figi"], 4393 "ticker": bond[1]["ticker"], 4394 "name": bond[1]["name"], 4395 "couponNumber": item["couponNumber"], 4396 "payOneBond": item["payOneBond"], 4397 "payCurrency": item["payCurrency"], 4398 "couponType": item["couponType"], 4399 "couponPeriod": item["couponPeriod"], 4400 "fixDate": item["fixDate"], 4401 "couponStartDate": item["couponStartDate"], 4402 "couponEndDate": item["couponEndDate"], 4403 } 4404 4405 if calendar is None: 4406 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4407 4408 else: 4409 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4410 4411 if calendar is not None: 4412 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4413 4414 # Saving calendar from Pandas DataFrame to XLSX sheet: 4415 if xlsx: 4416 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4417 4418 with pd.ExcelWriter( 4419 path=xlsxCalendarFile, 4420 date_format=TKS_DATE_FORMAT, 4421 datetime_format=TKS_DATE_TIME_FORMAT, 4422 mode="w", 4423 ) as writer: 4424 humanReadable = calendar.copy(deep=True) 4425 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4426 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4427 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4428 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4429 humanReadable.columns = colNames # human-readable column names 4430 4431 humanReadable.to_excel( 4432 writer, 4433 sheet_name="Bond payments calendar", 4434 index=False, 4435 encoding="UTF-8", 4436 freeze_panes=(1, 2), 4437 ) # saving as XLSX-file with freeze first row and column as headers 4438 4439 del humanReadable # release df in memory 4440 4441 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4442 4443 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4445 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4446 """ 4447 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4448 Also, creates Markdown file with calendar data, `calendar.md` by default. 4449 4450 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4451 4452 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4453 extended information about bonds: main info, current prices, bond payment calendar, 4454 coupon yields, current yields and some statistics etc. 4455 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4456 :param show: if `True` then also printing bonds payment calendar to the console, 4457 otherwise save to file `calendarFile` only. `False` by default. 4458 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4459 :return: multilines text in Markdown format with bonds payment calendar as a table. 4460 """ 4461 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4462 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4463 4464 infoText = "# Bond payments calendar\n\n" 4465 4466 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4467 4468 if not (calendar is None or calendar.empty): 4469 splitLine = "| | | | | | | | | |\n" 4470 4471 info = [ 4472 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4473 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4474 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4475 ] 4476 4477 newMonth = False 4478 notOneBond = calendar["figi"].nunique() > 1 4479 for i, bond in enumerate(calendar.iterrows()): 4480 if newMonth and notOneBond: 4481 info.append(splitLine) 4482 4483 info.append( 4484 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4485 " √" if bond[1]["paid"] else " —", 4486 bond[1]["couponDate"].split("T")[0], 4487 bond[1]["figi"], 4488 bond[1]["ticker"], 4489 bond[1]["couponNumber"], 4490 "{} {}".format( 4491 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4492 bond[1]["payCurrency"], 4493 ), 4494 bond[1]["couponType"], 4495 bond[1]["couponPeriod"], 4496 bond[1]["fixDate"].split("T")[0], 4497 ) 4498 ) 4499 4500 if i < len(calendar.values) - 1: 4501 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4502 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4503 newMonth = False if curDate.month == nextDate.month else True 4504 4505 else: 4506 newMonth = False 4507 4508 infoText += "".join(info) 4509 4510 if show and not onlyFiles: 4511 uLogger.info("{}".format(infoText)) 4512 4513 if self.calendarFile is not None and (show or onlyFiles): 4514 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4515 fH.write(infoText) 4516 4517 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4518 4519 if self.useHTMLReports: 4520 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4521 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4522 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4523 4524 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4525 4526 else: 4527 infoText += "No data\n" 4528 4529 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4531 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4532 """ 4533 Method for parsing and show simple table with all available user accounts. 4534 4535 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4536 4537 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4538 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4539 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4540 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4541 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4542 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4543 "closed": "—", "access": "Full access" }, ...}}` 4544 """ 4545 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4546 4547 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4548 accounts = { 4549 item["id"]: { 4550 "type": TKS_ACCOUNT_TYPES[item["type"]], 4551 "name": item["name"], 4552 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4553 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4554 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4555 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4556 } for item in rawAccounts["accounts"] 4557 } 4558 4559 # Raw and parsed data with some fields replaced in "stat" section: 4560 view = { 4561 "rawAccounts": rawAccounts, 4562 "stat": accounts, 4563 } 4564 4565 # --- Prepare simple text table with only accounts data in human-readable format: 4566 if show or onlyFiles: 4567 info = [ 4568 "# User accounts\n\n", 4569 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4570 "| Account ID | Type | Status | Name |\n", 4571 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4572 ] 4573 4574 for account in view["stat"].keys(): 4575 info.extend([ 4576 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4577 account, 4578 view["stat"][account]["type"], 4579 view["stat"][account]["status"], 4580 view["stat"][account]["name"], 4581 ) 4582 ]) 4583 4584 infoText = "".join(info) 4585 4586 if show and not onlyFiles: 4587 uLogger.info(infoText) 4588 4589 if self.userAccountsFile and (show or onlyFiles): 4590 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4591 fH.write(infoText) 4592 4593 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4594 4595 if self.useHTMLReports: 4596 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4597 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4598 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4599 4600 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4601 4602 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4604 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4605 """ 4606 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4607 4608 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4609 4610 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4611 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4612 :return: dict with raw parsed data from server and some calculated statistics about it. 4613 """ 4614 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4615 tmpTicker = self._ticker 4616 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4617 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4618 self._ticker = tmpTicker 4619 4620 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4621 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4622 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4623 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4624 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4625 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4626 4627 # This is dict with parsed common user data: 4628 userInfo = { 4629 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4630 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4631 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4632 "tariff": rawUserInfo["tariff"], 4633 } 4634 4635 # This is an array of dict with parsed margin statuses for every account IDs: 4636 margins = {} 4637 for accountId in accounts.keys(): 4638 if rawMargins[accountId]: 4639 margins[accountId] = { 4640 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4641 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4642 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4643 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4644 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4645 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4646 "missing": missing["volume"], 4647 } 4648 4649 else: 4650 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4651 4652 unary = {} # unary-connection limits 4653 for item in rawTariffLimits["unaryLimits"]: 4654 if item["limitPerMinute"] in unary.keys(): 4655 unary[item["limitPerMinute"]].extend(item["methods"]) 4656 4657 else: 4658 unary[item["limitPerMinute"]] = item["methods"] 4659 4660 stream = {} # stream-connection limits 4661 for item in rawTariffLimits["streamLimits"]: 4662 if item["limit"] in stream.keys(): 4663 stream[item["limit"]].extend(item["streams"]) 4664 4665 else: 4666 stream[item["limit"]] = item["streams"] 4667 4668 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4669 limits = { 4670 "unary": unary, 4671 "stream": stream, 4672 } 4673 4674 # Raw and parsed data as an output result: 4675 view = { 4676 "rawUserInfo": rawUserInfo, 4677 "rawAccounts": rawAccounts, 4678 "rawMargins": rawMargins, 4679 "rawTariffLimits": rawTariffLimits, 4680 "stat": { 4681 "overview": overview, 4682 "userInfo": userInfo, 4683 "accounts": accounts, 4684 "margins": margins, 4685 "limits": limits, 4686 }, 4687 } 4688 4689 # --- Prepare text table with user information in human-readable format: 4690 if show or onlyFiles: 4691 info = [ 4692 "# Full user information\n\n", 4693 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4694 "## Common information\n\n", 4695 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4696 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4697 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4698 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4699 "\n## User accounts\n\n", 4700 ] 4701 4702 for account in view["stat"]["accounts"].keys(): 4703 info.extend([ 4704 "### ID: [{}]\n\n".format(account), 4705 "| Parameters | Values |\n", 4706 "|----------------------|--------------------------------------------------------------|\n", 4707 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4708 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4709 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4710 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4711 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4712 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4713 ]) 4714 4715 if margins[account]: 4716 info.extend([ 4717 "| Margin status: | Enabled |\n", 4718 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4719 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4720 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4721 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4722 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4723 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4724 ]) 4725 4726 else: 4727 info.append("| Margin status: | Disabled |\n\n") 4728 4729 info.extend([ 4730 "\n## Current user tariff limits\n", 4731 "\n### See also\n", 4732 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4733 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4734 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4735 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4736 "\n### Unary limits\n", 4737 ]) 4738 4739 if unary: 4740 for key, values in sorted(unary.items()): 4741 info.append("\n* Max requests per minute: {}\n".format(key)) 4742 4743 for value in values: 4744 info.append(" - {}\n".format(value)) 4745 4746 else: 4747 info.append("\nNot available\n") 4748 4749 info.append("\n### Stream limits\n") 4750 4751 if stream: 4752 for key, values in sorted(stream.items()): 4753 info.append("\n* Max stream connections: {}\n".format(key)) 4754 4755 for value in values: 4756 info.append(" - {}\n".format(value)) 4757 4758 else: 4759 info.append("\nNot available\n") 4760 4761 infoText = "".join(info) 4762 4763 if show and not onlyFiles: 4764 uLogger.info(infoText) 4765 4766 if self.userInfoFile and (show or onlyFiles): 4767 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4768 fH.write(infoText) 4769 4770 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4771 4772 if self.useHTMLReports: 4773 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4774 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4775 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4776 4777 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4778 4779 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4782class Args: 4783 """ 4784 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4785 """ 4786 def __init__(self, **kwargs): 4787 self.__dict__.update(kwargs) 4788 4789 def __getattr__(self, item): 4790 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4793def ParseArgs(): 4794 """This function get and parse command line keys.""" 4795 parser = ArgumentParser() # command-line string parser 4796 4797 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4798 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4799 4800 # --- options: 4801 4802 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4803 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4804 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4805 4806 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4807 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4808 4809 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4810 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4811 4812 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4813 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4814 4815 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4816 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4817 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4818 4819 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4820 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4821 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4822 4823 # --- commands: 4824 4825 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4826 4827 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4828 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4829 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4830 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4831 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4832 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4833 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4834 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4835 4836 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4837 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4838 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4839 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4840 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4841 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4842 4843 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4844 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4845 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4846 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4847 4848 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4849 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4850 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4851 4852 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4853 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4854 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4855 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4856 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4857 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4858 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4859 4860 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4861 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4862 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4863 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4864 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4865 4866 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4867 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4868 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4869 4870 cmdArgs = parser.parse_args() 4871 return cmdArgs
This function get and parse command line keys.
4874def Main(**kwargs): 4875 """ 4876 Main function for work with TKSBrokerAPI in the console. 4877 4878 See examples: 4879 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4880 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4881 """ 4882 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4883 4884 if args.debug_level: 4885 uLogger.level = 10 # always debug level by default 4886 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4887 4888 exitCode = 0 4889 start = datetime.now(tzutc()) 4890 uLogger.debug("=-" * 50) 4891 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4892 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4893 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4894 )) 4895 4896 # trying to calculate full current version: 4897 buildVersion = __version__ 4898 try: 4899 v = version("tksbrokerapi") 4900 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4901 4902 except Exception: 4903 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4904 4905 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4906 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4907 4908 try: 4909 if args.version: 4910 print("TKSBrokerAPI {}".format(buildVersion)) 4911 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4912 4913 else: 4914 # Init class for trading with Tinkoff Broker: 4915 trader = TinkoffBrokerServer( 4916 token=args.token, 4917 accountId=args.account_id, 4918 useCache=not args.no_cache, 4919 ) 4920 4921 if args.tag is not None: 4922 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4923 4924 # --- set some options: 4925 4926 if args.more: 4927 trader.moreDebug = True 4928 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4929 4930 if args.html: 4931 trader.useHTMLReports = True 4932 4933 if args.ticker: 4934 ticker = str(args.ticker).upper() # Tickers may be upper case only 4935 4936 if ticker in trader.aliasesKeys: 4937 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4938 4939 else: 4940 trader.ticker = ticker 4941 4942 if args.figi: 4943 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4944 4945 if args.depth is not None: 4946 trader.depth = args.depth 4947 4948 # --- do one command: 4949 4950 if args.list: 4951 if args.output is not None: 4952 trader.instrumentsFile = args.output 4953 4954 trader.ShowInstrumentsInfo(show=True) 4955 4956 elif args.list_xlsx: 4957 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4958 4959 elif args.bonds_xlsx is not None: 4960 if args.output is not None: 4961 trader.bondsXLSXFile = args.output 4962 4963 if len(args.bonds_xlsx) == 0: 4964 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4965 4966 else: 4967 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4968 4969 elif args.search: 4970 if args.output is not None: 4971 trader.searchResultsFile = args.output 4972 4973 trader.SearchInstruments(pattern=args.search[0], show=True) 4974 4975 elif args.info: 4976 if not (args.ticker or args.figi): 4977 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4978 raise Exception("Ticker or FIGI required") 4979 4980 if args.output is not None: 4981 trader.infoFile = args.output 4982 4983 if args.ticker: 4984 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4985 4986 else: 4987 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4988 4989 elif args.calendar is not None: 4990 if args.output is not None: 4991 trader.calendarFile = args.output 4992 4993 if len(args.calendar) == 0: 4994 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4995 4996 else: 4997 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4998 4999 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 5000 5001 elif args.price: 5002 if not (args.ticker or args.figi): 5003 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5004 raise Exception("Ticker or FIGI required") 5005 5006 trader.GetCurrentPrices(show=True) 5007 5008 elif args.prices is not None: 5009 if args.output is not None: 5010 trader.pricesFile = args.output 5011 5012 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5013 5014 elif args.overview: 5015 if args.output is not None: 5016 trader.overviewFile = args.output 5017 5018 trader.Overview(show=True, details="full") 5019 5020 elif args.overview_digest: 5021 if args.output is not None: 5022 trader.overviewDigestFile = args.output 5023 5024 trader.Overview(show=True, details="digest") 5025 5026 elif args.overview_positions: 5027 if args.output is not None: 5028 trader.overviewPositionsFile = args.output 5029 5030 trader.Overview(show=True, details="positions") 5031 5032 elif args.overview_orders: 5033 if args.output is not None: 5034 trader.overviewOrdersFile = args.output 5035 5036 trader.Overview(show=True, details="orders") 5037 5038 elif args.overview_analytics: 5039 if args.output is not None: 5040 trader.overviewAnalyticsFile = args.output 5041 5042 trader.Overview(show=True, details="analytics") 5043 5044 elif args.overview_calendar: 5045 if args.output is not None: 5046 trader.overviewAnalyticsFile = args.output 5047 5048 trader.Overview(show=True, details="calendar") 5049 5050 elif args.deals is not None: 5051 if args.output is not None: 5052 trader.reportFile = args.output 5053 5054 if 0 <= len(args.deals) < 3: 5055 trader.Deals( 5056 start=args.deals[0] if len(args.deals) >= 1 else None, 5057 end=args.deals[1] if len(args.deals) == 2 else None, 5058 show=True, # Always show deals report in console 5059 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5060 ) 5061 5062 else: 5063 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5064 raise Exception("Incorrect value") 5065 5066 elif args.history is not None: 5067 if args.output is not None: 5068 trader.historyFile = args.output 5069 5070 if 0 <= len(args.history) < 3: 5071 dataReceived = trader.History( 5072 start=args.history[0] if len(args.history) >= 1 else None, 5073 end=args.history[1] if len(args.history) == 2 else None, 5074 interval="hour" if args.interval is None or not args.interval else args.interval, 5075 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5076 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5077 show=True, # shows all downloaded candles in console 5078 ) 5079 5080 if args.render_chart is not None and dataReceived is not None: 5081 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5082 5083 trader.ShowHistoryChart( 5084 candles=dataReceived, 5085 interact=iChart, 5086 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5087 ) 5088 5089 else: 5090 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5091 raise Exception("Incorrect value") 5092 5093 elif args.load_history is not None: 5094 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5095 5096 if args.render_chart is not None and histData is not None: 5097 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5098 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5099 5100 trader.ShowHistoryChart( 5101 candles=histData, 5102 interact=iChart, 5103 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5104 ) 5105 5106 elif args.trade is not None: 5107 if 1 <= len(args.trade) <= 5: 5108 trader.Trade( 5109 operation=args.trade[0], 5110 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5111 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5112 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5113 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5114 ) 5115 5116 else: 5117 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5118 5119 elif args.buy is not None: 5120 if 0 <= len(args.buy) <= 4: 5121 trader.Buy( 5122 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5123 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5124 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5125 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5126 ) 5127 5128 else: 5129 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5130 5131 elif args.sell is not None: 5132 if 0 <= len(args.sell) <= 4: 5133 trader.Sell( 5134 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5135 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5136 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5137 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5138 ) 5139 5140 else: 5141 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5142 5143 elif args.order: 5144 if 4 <= len(args.order) <= 7: 5145 trader.Order( 5146 operation=args.order[0], 5147 orderType=args.order[1], 5148 lots=int(args.order[2]), 5149 targetPrice=float(args.order[3]), 5150 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5151 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5152 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5153 ) 5154 5155 else: 5156 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5157 5158 elif args.buy_limit: 5159 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5160 5161 elif args.sell_limit: 5162 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5163 5164 elif args.buy_stop: 5165 if 2 <= len(args.buy_stop) <= 7: 5166 trader.BuyStop( 5167 lots=int(args.buy_stop[0]), 5168 targetPrice=float(args.buy_stop[1]), 5169 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5170 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5171 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5172 ) 5173 5174 else: 5175 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5176 5177 elif args.sell_stop: 5178 if 2 <= len(args.sell_stop) <= 7: 5179 trader.SellStop( 5180 lots=int(args.sell_stop[0]), 5181 targetPrice=float(args.sell_stop[1]), 5182 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5183 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5184 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5185 ) 5186 5187 else: 5188 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5189 5190 # elif args.buy_order_grid is not None: 5191 # # update order grid work with api v2 5192 # if len(args.buy_order_grid) == 2: 5193 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5194 # 5195 # for order in orderParams: 5196 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5197 # 5198 # else: 5199 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5200 # 5201 # elif args.sell_order_grid is not None: 5202 # # update order grid work with api v2 5203 # if len(args.sell_order_grid) >= 2: 5204 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5205 # 5206 # for order in orderParams: 5207 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5208 # 5209 # else: 5210 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5211 5212 elif args.close_order is not None: 5213 trader.CloseOrders(args.close_order) # close only one order 5214 5215 elif args.close_orders is not None: 5216 trader.CloseOrders(args.close_orders) # close list of orders 5217 5218 elif args.close_trade: 5219 if not (args.ticker or args.figi): 5220 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5221 raise Exception("Ticker or FIGI required") 5222 5223 if args.ticker: 5224 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5225 5226 else: 5227 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5228 5229 elif args.close_trades is not None: 5230 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5231 5232 elif args.close_all is not None: 5233 if args.ticker: 5234 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5235 5236 elif args.figi: 5237 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5238 5239 else: 5240 trader.CloseAll(*args.close_all) 5241 5242 elif args.limits: 5243 if args.output is not None: 5244 trader.withdrawalLimitsFile = args.output 5245 5246 trader.OverviewLimits(show=True) 5247 5248 elif args.user_info: 5249 if args.output is not None: 5250 trader.userInfoFile = args.output 5251 5252 trader.OverviewUserInfo(show=True) 5253 5254 elif args.account: 5255 if args.output is not None: 5256 trader.userAccountsFile = args.output 5257 5258 trader.OverviewAccounts(show=True) 5259 5260 else: 5261 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5262 raise Exception("There is no command to execute") 5263 5264 except Exception: 5265 trace = tb.format_exc() 5266 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5267 if e in trace: 5268 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5269 break 5270 5271 uLogger.debug(trace) 5272 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5273 exitCode = 255 # an error occurred, must be open a ticket for this issue 5274 5275 finally: 5276 finish = datetime.now(tzutc()) 5277 5278 if exitCode == 0: 5279 if args.more: 5280 uLogger.debug("All operations were finished success (summary code is 0).") 5281 5282 else: 5283 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5284 os.path.abspath(uLog.defaultLogFile), exitCode, 5285 )) 5286 5287 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5288 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5289 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5290 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5291 )) 5292 uLogger.debug("=-" * 50) 5293 5294 if not kwargs: 5295 sys.exit(exitCode) 5296 5297 else: 5298 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples:
